返回 DeepSeek-Reasonix
SPEC.md
根目录 / docs / SPEC.md
1 # Reasonix Engineering Spec
2
3 > Reasonix is a coding agent: a thin harness driving multiple models, with **all
4 > capabilities supplied by configuration and plugins**. This document is the
5 > contract — code follows it. Change the contract first, then the code.
6
7 The current file-operation, scheduling, and interruption contract is specified
8 in [Harness-style execution migration](DSH_EXECUTION_MIGRATION.md).
9
10 ## 1. Design Principles
11
12 1. **Config- and plugin-driven core.** The core knows only interfaces. Concrete
13 models and tools are resolved by name from registries, declared in config, or
14 injected by plugins. No hardcoded `switch model`.
15 2. **Single static binary.** `CGO_ENABLED=0`; cross-compile with one command;
16 CLI works out of the box.
17 3. **Lean dependencies.** Standard library by default. A third-party dependency
18 must be pure-Go, lightweight, and must not compromise the single-binary /
19 cross-platform / distribution story. TOML parsing is the one accepted dependency.
20 4. **Two extension tiers.** Compile-time built-ins (self-register via `init()`),
21 and runtime external plugins (stdio JSON-RPC subprocesses, MCP-compatible).
22 5. **Interface-first & registry-based.** `Provider` and `Tool` are interfaces.
23 6. **Evolve, don't over-engineer.**
24
25 Language: **English is the primary language for all code** — comments,
26 user-facing strings, tool descriptions, system prompts, and this spec. The
27 README is bilingual (`README.md` English + `README.zh-CN.md`).
28
29 ## 2. Layout
30
31 ```
32 reasonix/
33 ├── go.mod / go.sum # module reasonix; require BurntSushi/toml
34 ├── Makefile # build / cross / vet / fmt / test
35 ├── README.md / README.zh-CN.md
36 ├── reasonix.example.toml # sample config
37 ├── docs/SPEC.md # this file
38 ├── cmd/reasonix/main.go # entry; blank-imports built-in providers/tools
39 ├── cmd/reasonix-plugin-example/ # reference MCP stdio plugin (a runnable example)
40 └── internal/
41 ├── cli/ # subcommand routing, flags, assembly, exit codes
42 ├── config/ # TOML loading (flag > project > user > defaults)
43 ├── provider/ # Provider interface + types + kind→factory registry
44 │ └── openai/ # OpenAI-compatible impl; init() registers "openai"
45 ├── tool/ # Tool interface + Registry
46 │ └── builtin/ # read_file/write_file/edit_file/move_file/bash/ls/glob/grep
47 ├── permission/ # per-call Policy: allow/ask/deny rules → Decision
48 ├── command/ # custom slash commands loaded from .reasonix/commands/*.md
49 ├── plugin/ # stdio JSON-RPC (MCP) client; adapts remote tools
50 ├── remote/ # SSH transport for the Remote-SSH module
51 │ ├── forward/ # -L / -R port-forward lifecycle
52 │ ├── sftpfs/ # SFTP file layer (quarantines pkg/sftp)
53 │ └── bootstrap/ # detached `reasonix serve` bootstrap over SSH
54 └── agent/ # Session + harness loop
55 ```
56
57 Dependency direction (acyclic): `cli → {agent, plugin, config} → {tool, provider}`.
58 Built-in subpackages (`provider/openai`, `tool/builtin`) import their parent to
59 self-register; parents never import children. The Remote-SSH module layers
60 `cli → remote/bootstrap → remote → {remote/forward, remote/sftpfs, config,
61 netclient}`; `remote` and its subpackages never import `cli`, `agent`, or
62 `serve`, and all interactivity flows through callbacks (host-key / secret
63 prompts) so the desktop module consumes the same surface. See the
64 [Remote sessions](./REMOTE_SESSIONS.md) guide.
65
66 ## 3. Core Abstractions
67
68 ### 3.1 Provider + registry (`internal/provider`)
69
70 ```go
71 type Provider interface {
72 Name() string
73 Stream(ctx context.Context, req Request) (<-chan Chunk, error)
74 }
75
76 // Factory builds a Provider from a resolved config instance.
77 type Factory func(cfg Config) (Provider, error)
78
79 // Register adds a factory under a kind (e.g. "openai"). Called from init().
80 func Register(kind string, f Factory)
81
82 // New instantiates the provider of the given kind.
83 func New(kind string, cfg Config) (Provider, error)
84
85 type Config struct {
86 Name string // instance name, e.g. "deepseek"
87 BaseURL string
88 Model string
89 APIKey string
90 Extra map[string]any // kind-specific options
91 }
92 ```
93
94 - The `openai` kind is an OpenAI-compatible `/chat/completions` implementation.
95 - **OpenAI-compatible vendors are config instances** of `kind = "openai"`,
96 differing only in `base_url` / `model` / `api_key_env`. Adding another OpenAI-
97 compatible model is a config edit, not a code change.
98 - **A provider is a vendor endpoint** (one `base_url` + `api_key_env`) that offers
99 one or more models. `request_url`, when set, is the exact request target for
100 OpenAI-compatible, Anthropic-compatible, and Responses providers. Legacy
101 `chat_url` retains its historical OpenAI-only behavior; other legacy entries
102 derive the protocol path from `base_url`. An entry declares either a single `model = "..."` or a
103 `models = ["...", "..."]` list (with an optional `default`); the list form lets
104 one vendor expose several models without re-declaring the endpoint/key. A
105 **model reference** (`default_model`, the `--model` flag, the desktop switcher)
106 resolves via `Config.ResolveModel`, which accepts a provider name (→ its default
107 model), a bare model name, or an explicit `provider/model`. `context_window` is
108 the provider-wide fallback; `model_overrides.<model>.context_window` can replace
109 it for one model. Per-model `prices` use model IDs as keys.
110 - Streaming tool-call deltas are accumulated by index inside the provider; only
111 complete `ToolCall`s are emitted.
112
113 ### 3.2 Tool + registry (`internal/tool`)
114
115 ```go
116 type Tool interface {
117 Name() string
118 Description() string
119 Schema() json.RawMessage // JSON Schema for parameters
120 Execute(ctx context.Context, args json.RawMessage) (string, error)
121 }
122 ```
123
124 - Built-in tools self-register into a process-global builtin set via `init()`
125 (`tool.RegisterBuiltin(t)`); `tool.Builtins()` lists them.
126 - A runtime `*Registry` is assembled per run: enabled built-ins (filtered by
127 config) **plus** plugin-provided tools. The agent only sees the `*Registry`.
128 - Tool schemas are canonicalized on registry insertion. The built-in contract is
129 documented in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md) and backed by tests that
130 compare the documented surface against the same canonical schema path.
131 - `Execute` parses raw JSON args itself. Errors are returned, not fatal — the
132 agent feeds them back so the model can self-correct.
133
134 ### 3.3 Plugins (`internal/plugin`) — MCP client
135
136 An external plugin is an MCP server declared in config. The wire protocol is
137 **JSON-RPC 2.0** in every case; only the transport differs. Reasonix keeps the
138 product-level client and delegates protocol negotiation, request correlation,
139 cancellation, pagination, and transport framing to the official MCP Go SDK.
140 One concurrency-safe session per configured server is shared by tools, prompts,
141 and resources.
142
143 - **Transports** (config `type`):
144 - `stdio` (default) — a local subprocess; one JSON message per line over the
145 child's stdin/stdout (the MCP stdio convention). Declared with
146 `command` / `args` / `env`; terminated on ctx cancel / shutdown.
147 - `http` (a.k.a. `streamable-http`) — a remote server at `url`. After
148 initialize, a long-lived GET/SSE listener receives server messages while
149 POST carries client requests; POST-only and sessionless servers remain
150 supported. The `Mcp-Session-Id` response header, once seen, is echoed on
151 subsequent GET, POST, and bounded shutdown DELETE requests. Static
152 `headers` (e.g. a bearer token) are sent to the configured origin on each
153 transport method and are never forwarded cross-origin. When no static
154 `Authorization` header is configured,
155 user-initiated OAuth uses Protected Resource Metadata and Authorization
156 Server Metadata discovery, dynamic client registration, PKCE S256, a
157 loopback callback, resource indicators, and refresh-token rotation. Client
158 credentials and tokens are stored with mode `0600` in the server's private
159 Reasonix MCP state directory, outside the workspace; tokens are bound to the
160 configured resource URL and are never reused after that URL changes. OAuth
161 discovery, registration, and token requests honor Reasonix's resolved
162 network-proxy settings. Removing a declaration clears this state unless the
163 effective fallback uses the same OAuth resource.
164 - `sse` — the legacy 2024-11-05 HTTP+SSE transport. A persistent GET stream
165 receives an announced relative POST endpoint, JSON-RPC responses, and server
166 messages. Cross-origin announced endpoints are rejected so static headers
167 cannot leak.
168 - `${VAR}` / `${VAR:-default}` are expanded in `command`, `args`, `env`, `url`,
169 and `headers` so secrets come from the environment, not the config file.
170 - Lifecycle: `initialize` → `notifications/initialized` → `tools/list`;
171 invocation via `tools/call {name, arguments}`.
172 - A per-server supervisor publishes only fully initialized/listening sessions.
173 An established session that returns 404 is rebuilt once with concurrent
174 callers joining the same rebuild; a call is replayed at most once. Ambiguous
175 disconnects never replay tool calls because the server may already have
176 executed them. Terminal background disconnects use bounded reconnect delays,
177 and stale callbacks from an older session generation cannot replace current
178 state.
179 - When a workspace root exists, initialize advertises `roots` and transports
180 answer `roots/list` with its file URI. `tools/call` includes a per-call
181 `_meta.progressToken`; matching `notifications/progress` messages stream into
182 the existing tool-progress event path.
183 - A stdio server uses one persistent transport for initialize, reads, and
184 writes, preserving state such as browser sessions across tool calls. The
185 process uses the server's process sandbox because process confinement cannot
186 change per RPC; read-only eligibility and destructive filtering remain local
187 workflow gates rather than separate process sandboxes.
188 - Configuration provenance is runtime metadata and determines persistence scope.
189 Desktop and CLI installs write the user-global `config.toml`; project
190 `reasonix.toml` and `.mcp.json` entries remain in their owning project file.
191 Every configured source is trusted without a separate launch-confirmation
192 step. Project entries override same-name global entries, and project
193 `reasonix.toml` overrides `.mcp.json`. Editing writes to the effective entry's
194 source; removing it reveals the next lower-priority declaration.
195 - Each remote tool is adapted to the `Tool` interface and injected into the run
196 registry, namespaced `mcp__<server>__<tool>` (spaces normalised to `_`) to
197 match Claude Code and avoid clashes.
198 - A tool's MCP `annotations.readOnlyHint` maps to `Tool.ReadOnly()`. It defaults
199 to false (a remote tool is opaque — we can't see its side effects), so a
200 plugin opts a tool into parallel-batch dispatch and the permission layer's
201 reader-default by declaring `readOnlyHint: true` in `tools/list`.
202 - Installation is the trust decision for tool metadata. Reasonix assumes an
203 installed server reports `readOnlyHint` and `destructiveHint` honestly;
204 planner/read-only filtering is a workflow boundary for trusted servers, not
205 containment against a malicious MCP server. Explicit deny rules and the
206 process sandbox remain host-controlled boundaries.
207 - `prompts/list` + `prompts/get` surface as `/mcp__<server>__<prompt>` slash
208 commands; `resources/list` + `resources/read` are referenced as
209 `@<server>:<uri>` in chat. All list cursors are consumed while preserving
210 server order. `/mcp` shows connected servers, counts, protocol/listening state,
211 reconnect attempts, and a redacted error category; it never exposes a session
212 identifier.
213 - `cmd/reasonix-plugin-example` is a runnable reference stdio server (`echo`,
214 `wordcount`), driven by an end-to-end test that builds the real binary.
215
216 ### 3.4 Agent (`internal/agent`)
217
218 - `Session` holds `[]Message`.
219 - `Run(ctx, input)` loop: build `Request` (with tool schemas) → `provider.Stream`
220 → print text deltas live, collect complete tool calls → if none, done; else
221 execute each tool (built-in or plugin) and append results → repeat, bounded by
222 `maxSteps`. `ctx` threads throughout (Ctrl-C aborts in-flight requests).
223 - A `Runner` is anything with `Run(ctx, input) error`; both `Agent` and
224 `Coordinator` satisfy it, so the CLI is agnostic to single- vs two-model mode.
225
226 ### 3.5 Two-model collaboration (`Coordinator`)
227
228 When `agent.planner_model` is set, a `Coordinator` runs two models in
229 **separate sessions** to keep each one's prompt prefix cache-stable. An empty
230 `planner_model` leaves the session executor-only. A configured but unusable
231 planner model is a configuration error and does not silently continue on the
232 executor:
233
234 - The **planner** (low-frequency) runs in its own session with the same standing
235 memory context plus a filtered read-only research tool set, then produces a
236 concise plan. A deterministic host policy defaults to executor-only. It
237 invokes the dedicated planner only for an explicit plan-first /
238 plan-then-execute request, an explicit wait-for-approval boundary, an
239 explicit plan-only request, or an explicit Goal start. It does not call a
240 classifier model, does not infer complexity from wording, file count, or
241 keywords, and does not infer host state from controller-authored prompt
242 blocks. Explicit Plan Mode is an executor-driven workflow and never starts a
243 second planner. Synthetic turns, short contextual replies, and ordinary
244 requests stay executor-only. There is no Light/Full planning depth. The
245 privacy-safe route/reason decision is emitted in phase detail.
246 - The planner uses one stable system prompt. Only a small host-authored
247 `<planner-turn>` block names the explicit route. The plan distinguishes
248 verified from candidate touchpoints and records non-goals, risks, acceptance
249 criteria, and command-level verification when the evidence supports them.
250 `submit_plan` is the only delivery channel; a prose reply without a submitted
251 plan is a planner protocol error. If the planner still does not finalize after
252 the bounded research and grace round, every route fails closed and the
253 executor is not started. The incomplete planner turn is rolled back rather
254 than exposed as a broken manual continuation.
255 - A bare plan-first route hands the completed plan directly to the executor.
256 Plan-for-approval is reserved for an explicit request to wait for
257 confirmation; the host enforces that boundary even if the planner omits its
258 marker, then hands the approved plan to the executor. A headless host persists
259 the plan so a later turn can continue. Explicit plan-only requests persist the
260 plan and end the current turn without execution. A planner failure on either
261 execution boundary cannot fall back to the executor. These directives may
262 appear after the task clause; quoted examples do not change the route.
263 - The plan is handed off as structured text to the **executor** — a full
264 tool-using `Agent` in its own session — which validates candidate assumptions
265 and carries it out.
266 - The sessions never mix, so neither model's prefix is disturbed by the other's
267 turns; both grow prepend-only and stay cache-friendly. This reconciles
268 "cache-first" with "two-model collaboration": switching models *inside one
269 shared conversation* would break the prefix and tank cache hits, so we don't.
270
271 ### 3.6 Context management (content-driven summary)
272
273 Long tasks fill the model window. Reasonix keeps a **cache-first, append-only**
274 canonical transcript and installs a short **provider-visible checkpoint** only
275 when the sole automatic threshold is crossed.
276
277 - Each provider declares `context_window` (tokens). The only automatic trigger is
278 `agent.compact_ratio` (default **0.80**; presets 0.70 / 0.80 / 0.85; range
279 0.30–0.85). Lower values compact sooner and may increase summary cost or
280 reduce prompt-cache reuse.
281 `triggerTokens = floor(context_window × compact_ratio)`.
282 - **Below the trigger** ordinary requests remain append-only and no sidecar is
283 written. Every provider request uses the durable, bounded tool `Content`;
284 local `RawContent` is never promoted into sampling, retry, summary, or replay.
285 - **At the trigger** one singleflight maintenance transaction first persistently
286 prunes every tool result over 8192 Unicode code points to `4096 head +
287 "[... tool result middle pruned ...]" + 1024 tail`. If this clears pressure,
288 no summary request is made. Otherwise Reasonix summarizes the old contiguous
289 prefix and retains the newest **16%** of the context window verbatim, aligned so
290 assistant tool calls and tool results are never split.
291 - The summary request replays the original system message, the selected message
292 prefix, and the ordinary request's tool schemas, then appends one final user
293 compaction instruction. This shape can reuse provider KV cache. Output is capped
294 at **8192 tokens**, and prefix planning keeps **5%** of the window (at least 256
295 tokens) below that cap as estimator headroom. A pressure run may make one
296 additional convergence summary (at most two successful summaries total);
297 overflow makes at most one summary and retries the original request at most
298 once after projection-version progress. An overflow rescue may also fold the
299 active turn's completed rounds, keeping its newest two rounds verbatim.
300 - Every summary reply, success or provider overflow, feeds its real prompt count
301 back into the estimator. When the provider rejects the summary request itself,
302 the fold is re-planned on the corrected estimate (at most twice), then sent once
303 as a bounded transcript (tool results cut to 2000 characters, no tool schemas);
304 a manual compact may then take the fragment path. A failed automatic attempt
305 backs off further attempts on the same turn until the view has grown by 5% of
306 the window since that attempt, which bounds the retries one turn can pay.
307 - A checkpoint must be strictly smaller than the replaced full request. Summary
308 timeout/error/empty/max-token results never produce a mechanical digest. Below
309 the hard ceiling the latest durable projection continues. At overflow or the
310 hard ceiling, when no summary can form, a lossy `truncate` projection elides the
311 oldest tool results and then drops the oldest replay units behind an explicit
312 marker until the view fits under the trigger; `ErrCompactionRequired` is
313 returned only when even that cannot reclaim enough.
314 - Users inspect or change the threshold with
315 `reasonix config compact-ratio [--local] [VALUE]`. Project config overrides the
316 user-global value used by desktop and new CLI sessions. UI always shows the
317 **effective** ratio.
318 - `max_output_tokens` is an independent **per-turn** completion ceiling and
319 never changes `triggerTokens` / `compact_ratio`.
320 - `0` is the provider auto value. Local admission uses the provider
321 capability (official DeepSeek 384K, OpenCode Go model table, or a learned
322 completion budget). It is **not** “skip the local output check”.
323 - Official DeepSeek Chat/Responses still omit the field when the remaining
324 shared window can host the 384K auto budget, and inject a clipped value
325 only when the window is tight. Official DeepSeek Anthropic always sends
326 384K or the clipped remainder because `max_tokens` is required.
327 - Official OpenCode Go presets send `min(model max, physical remaining)` on
328 the generic `max_tokens` / `max_output_tokens` field. Third-party
329 compatible APIs do not assume a shared window until a trusted context 400.
330 - A positive value is an explicit cost cap and may still be clipped down to
331 the physical remainder. A negative value force-omits optional wire limits;
332 if the known auto budget no longer fits, Reasonix compacts instead of
333 overriding that choice.
334 - Canonical tool storage remains backward compatible: `Content` is the stable
335 provider-visible ≤32KB form and `RawContent` holds the full local original.
336 Full results are returned to the model only after an explicit paged
337 `use_capability` call to `session:tool_result`; sampling, stream retry, summary,
338 and projection replay all use the same bounded `Content`. Prune projections
339 never rewrite either canonical field. Older supported readers remain bounded.
340 - Automatic maintenance is planned once in `ContextManager.Prepare` from the
341 current projection plus the append-only canonical tail. The canonical
342 transcript is never rewritten. Subsequent thresholds merge
343 **prior digest + new history** into a single digest (no multi-span merge, no
344 application-layer retry). Failure records a generation-scoped
345 `blocked`/`failed` receipt; the same generation does not pay for another
346 automatic summary. Manual `compress` can retry.
347 - Old multi-threshold keys (`soft_compact_ratio`, `tool_result_snip_ratio`,
348 `compact_force_ratio`, `cold_resume_prune`, `context_editing`) are removed on
349 ordinary start and ignored at runtime. Native provider tool clearing is not
350 used; every provider uses the local summary checkpoint path.
351 - `keep` / `recent_keep` remain readable and round-trip for compatibility but are
352 deprecated and ignored by compaction. Old user turns, failed tool results, and
353 `[[keep]]` messages enter the summary prefix. Restart restores an existing
354 checkpoint without re-summarizing or replaying timeline cards.
355 - Full history remains in the session transcript. The read-only `history` tool
356 provides BM25 retrieval over sessions; new summary checkpoints do not create
357 prune archives.
358 - The read-only `history` tool gives the agent on-demand BM25 retrieval over
359 saved session JSONL files. `scope="project"` searches the current controller's
360 session directory; `scope="global"` also searches the user-global session
361 directory and compacted-history archives. `operation="around"` can then read a
362 bounded transcript window around a returned hit. Search keeps the best hit and
363 trims trailing common-word-only noise with a relative score floor; a 0-result
364 response tells the agent how to retry with rarer terms or widen scope.
365 - The read-only `memory` tool gives the agent on-demand search/list/read access
366 to saved auto-memory files. It complements the writer tools: `memory` checks
367 what already exists, `remember` saves or updates a fact, and `forget` removes
368 a stale one from the active index while archiving the file for traceability.
369 Archived memory files are visible in local management surfaces (`/memory`,
370 TUI, desktop panel) but are excluded from active-memory retrieval. Memory
371 search uses the same relative BM25 floor and guides the agent to fall back to
372 history when exact original wording or tool output matters.
373 - Before each real user turn, bounded BM25 recall selects relevant active facts
374 from the raw user message and appends them as a low-authority user-turn suffix.
375 Generic turns are suppressed, project facts override equivalent global
376 fallbacks, stale facts are down-ranked, and recall is bounded by result/character
377 budgets. This never mutates the stable system prompt or tool schemas.
378 - The owning controller may auto-allow only a bounded, non-sensitive,
379 create-only project/reference `remember`, including in a top-level headless
380 run. Other memory writes follow the active permission preset and preserve
381 explicit `ask` and `deny` rules. Full access bypasses ordinary prompts unless
382 an explicit deny rule matches.
383 Guardian/safety review cannot answer these prompts on the user's
384 behalf. Sub-agents and headless surfaces without the owning scoped
385 controller fail closed, including headless execution except for the create-only
386 path above. The approval request includes a compact preview, while
387 external notification hooks only receive the tool name.
388 - Facts carry immutable IDs, monotonic revisions, timestamps, type, and scope.
389 Updates snapshot the previous revision; restore and archive recovery create a
390 higher revision and reject path escapes, symlinks, collisions, and overwrites.
391 User-initiated memory edits in the local UI are already explicit user actions.
392 See [`SESSION_MEMORY_RETRIEVAL.md`](SESSION_MEMORY_RETRIEVAL.md) for the
393 detailed implementation contract.
394
395 **What survives a fold.** The system prompt and newest 16% tail survive verbatim.
396 Every older model-visible message forms one contiguous summary prefix, including
397 user turns, failed tool results, prior digests, and `[[keep]]` messages. Exact
398 older wording remains available in the canonical transcript and through the
399 read-only `history` tool. `keep` and `recent_keep` are compatibility-only fields.
400
401 Subsequent folds merge the current digest with newer old history into one digest.
402 Compaction only writes a projection: canonical storage keeps every original, so
403 a missed detail stays recoverable through `history`.
404
405 Prune and summary commits are deliberate cache-reset points. Between maintenance
406 runs the session remains append-only and cache-friendly. `context_window = 0`
407 disables automatic compaction for an instance.
408
409 ### 3.7 Permissions (`internal/permission`) — per-call gating
410
411 A coding agent runs shell commands and edits files autonomously. The permission
412 layer decides, **per tool call**, whether to allow it, deny it, or ask the user
413 first. It is independent of the model and of the CLI — the agent consults a
414 `Gate` interface at execute time; the gate is built from a static `Policy` plus
415 an optional interactive `Approver`.
416
417 ```go
418 type Decision int // permission package
419 const (Allow Decision = iota; Ask; Deny)
420
421 // Policy evaluates static rules against a tool call. Pure, no I/O.
422 type Policy struct { Mode Decision; Allow, Ask, Deny []Rule }
423 func (p Policy) Decide(toolName string, readOnly bool, args json.RawMessage) Decision
424 ```
425
426 - **Rule syntax.** A rule is `Tool` (matches any call in that tool family) or
427 `Tool(specifier)` (matches when the call's *subject* matches the specifier).
428 Bash and file mutation approvals use Claude Code-style families such as
429 `Bash(npm run build)`, `Bash(npm run test:*)`, and `Edit(docs/**)`. Built-in
430 file mutations include writes, edits, notebook edits, symbol/range deletes,
431 and `move_file` renames/moves. Legacy lowercase tool IDs still load for
432 compatibility. `Bash=<literal>` is the exact-command form: metacharacters in
433 the literal are ordinary characters and only the identical complete command
434 matches. The
435 `:*` suffix marks a Bash command-prefix approval; generated prefix rules also
436 reject later commands that introduce shell operators, so `Bash(go test:*)`
437 does not cover `go test ./... && rm -rf tmp`.
438 Legacy `Bash(go test *)` prefix rules still load, but new rules are saved as
439 `Bash(go test:*)`. The subject is extracted generically from the call's JSON
440 args by a small set of
441 known keys — `command` (bash), `path` / `file_path` (file tools), `pattern`
442 (grep/glob) — so tools need not change. A rule whose subject the args don't
443 expose only matches in its bare `Tool` form.
444 - **Shell syntax.** Pipes, substitutions, redirects, shell `-c`, and runtime
445 inline-code flags follow the active permission preset and OS sandbox. Syntax
446 never creates a separate approval rule. Explicit `deny` rules and exact
447 session grants continue to match the canonical command subject.
448 - **Precedence.** `deny` > `ask` > `allow` > fallback. Fallback is `Allow` for
449 read-only tools and `Mode` (default `Ask`) for writers. `deny` always wins, so
450 a broad `allow = ["Bash"]` can still be carved by `deny = ["Bash(rm -rf*)"]`;
451 conversely `ask` overrides a broad `allow` to force a prompt on a risky subset.
452 - **Resolving authorization.** The interactive frontend offers allow once,
453 allow the displayed scope for this session, or deny. Session grants bind an
454 exact command, canonical directory, or server capability and are never written
455 to project configuration. A non-interactive run cannot prompt and therefore
456 fails closed when its preset does not cover the operation. A `Deny` is a hard
457 block in every preset.
458 - **MCP authorization.** Installing an MCP server authorizes all of its tools;
459 there is no second server, raw-tool, writer, or destructive approval policy.
460 Project configuration is trusted the same way and requires no separate launch
461 confirmation. Explicit global deny rules still win. `readOnlyHint` and
462 `destructiveHint` remain internal
463 facts for scheduling, Plan/read-only restrictions, and cached-to-live safety
464 reclassification. Strict read-only sub-agent registries expose only
465 authorized tools with `readOnlyHint: true` and no `destructiveHint`. The
466 two-model Planner uses the fixed `use_capability` proxy (never direct
467 `mcp__*` schemas) for authorized, non-destructive MCP without requiring
468 `readOnlyHint`; destructive tools are left for the Executor. In Balanced
469 two-model sessions the Executor owns an isolated frontend for the same proxy,
470 so Planner-discovered capability IDs remain executable after handoff. Schema-only
471 changes refresh the next-session cache without adding an execution approval
472 or retry. Immediately before dispatch, the proxy re-checks the current
473 controller's enablement, authorization, and complete runtime connection
474 identity; a same-name client on a shared Host is never sufficient authority.
475 - **Relationship to plan mode.** Plan mode (§3.4) is a plan-first collaboration
476 workflow, not an all-tools read-only mode. Before Permissions/Sandbox, the
477 host enforces explicit phase opt-outs. The dedicated two-model Planner may call authorized,
478 non-destructive MCP even when `readOnlyHint` is absent; it hard-blocks
479 destructive targets and readers from unauthorized servers for the entire
480 planning phase. A single-model Plan without the dedicated Planner continues
481 to block MCP writer/destructive targets while Plan is active.
482 Ordinary built-in and Bash calls then use the same Read only, Workspace
483 access, or Full access preset, explicit `ask`/`deny`, and OS sandbox path as
484 Standard mode. A third-party MCP
485 `readOnlyHint` affects dispatch classification and strict-child eligibility,
486 but not the dedicated Planner's non-destructive trust path. Once the server is
487 installed or declared in project configuration, all non-destructive
488 capabilities enter the dedicated Planner proxy; only hinted readers enter
489 strict read-only sub-agent execution. `plan_mode_read_only_commands` is
490 retained for config/session round trips and does not grant or revoke calls in
491 the main Plan workflow. `read_only_task` and `read_only_skill` remain strict
492 read-only capabilities with their own tool registry and safe foreground Bash;
493 writer-capable `task` and skill execution remain permission-gated instead of
494 Plan-blocked, and their child turns inherit the Plan workflow marker and
495 explicit phase opt-outs.
496 - **User decisions are separate from tool approvals.** Runtime permission has
497 three presets: `read-only`, `workspace-write`, and `danger-full-access`.
498 Workspace write is the default and confines local mutations to the workspace
499 and private session temporary directory. Full access skips ordinary prompts
500 and Reasonix filesystem/network confinement. Explicit host deny rules still
501 run before launch, but Reasonix does not constrain the launched process.
502 Shell syntax does not alter the selected preset.
503 Neither posture answers `ask` questions or approves `exit_plan_mode` plans.
504 Plan Mode is entered only through an explicit user choice and remains
505 independent of the active tool-approval posture. After a user approves a
506 plan, the controller opens a short `approvedPlanAutoApproveTools` execution
507 window so the model can perform the approved writes without re-prompting; that
508 transient window still does not auto-approve future plans. In headless `ask`
509 execution, any fallback answer is labelled as a model assumption, not as a
510 user decision.
511
512 - **Collaboration mode is separate from tool approval.** The desktop composer
513 presents collaboration as `normal` ("正常模式"), `plan` ("计划模式"), and
514 `goal` ("目标模式"). `/goal <objective>` starts an autonomous, session-scoped
515 active goal: stable lifecycle-tool rules stay in the cacheable system prefix,
516 while each automatic round carries the escaped objective and exact goal
517 identity as dynamic user input. A runtime-idle driver admits one normal
518 top-level turn at a time until the model completes or blocks the goal, the
519 user pauses or clears it, or an explicit resource boundary is reached. An
520 automatic blocked transition is rejected before three admitted goal rounds;
521 deciding whether the same blocker persisted is the model's responsibility,
522 not a second host detector. A
523 goal is treated as a task contract: if the objective includes Context,
524 Request, Output format, Constraints, or Pause policy sections, those sections
525 define the autonomous work boundary. When they are absent, the model infers a
526 lightweight contract from the conversation and workspace. The injected goal
527 block tells the model to pause only for irreversible or externally visible
528 operations, scope changes, or information only the user can provide; ordinary
529 uncertainty should be handled with sensible defaults and reported as an
530 assumption. Completion requires the concrete request, output format,
531 constraints, and relevant verification expectations to be satisfied or
532 explicitly reported as unverified.
533 Goal has no default model-round, cross-Run turn, wall-clock, or numeric
534 no-progress boundary. Exact consecutive tool calls receive bounded reminders
535 and still execute. The model-facing lifecycle is `get_goal`, `create_goal`
536 and versioned `update_goal(edit|pause|resume|complete|blocked)`. There is no
537 `continue` action: `active + armed` is sufficient for the idle driver. There
538 is no host readiness evaluator, second research protocol or writable sidecar
539 runtime. Legacy `.reasonix/autoresearch/...`
540 archives remain read-only and explicit old paths recover as ordinary Goals.
541 Ordinary prompts never force collaboration mode at the host level, although
542 the model may create a long-running goal from a directly authorized human
543 request when its semantics require autonomous continuation.
544 Turns,
545 tokens, provider requests, and active work duration remain observational when
546 the corresponding budget is not configured. Positive user-selected
547 `[agent].goal_token_budget`, `max_steps`, time, and cost budgets remain
548 explicit resumable boundaries. The Goal token budget defaults to `0` (off);
549 resuming a `budget_spend` pause grants a fresh slice without clearing
550 cumulative Goal statistics. `task_time_budget_minutes = 0` (and legacy
551 negative values) disables the time boundary.
552 `/goal clear` removes the active goal. Switching into plan/normal mode clears
553 the active goal in the desktop UI so the collaboration mode remains one of
554 the three choices, while the underlying tool approval posture is preserved.
555
556 | Permission preset | Tool authorization | Plan approval | `ask` questions |
557 | --- | --- | --- | --- |
558 | Read only / `read-only` | Reads are allowed; writes and external side effects require a scoped authorization | Waits for user | Waits for user |
559 | Workspace access / `workspace-write` | Workspace and private session temp writes run inside the OS sandbox; boundary crossings require authorization | Waits for user | Waits for user |
560 | Full access / `danger-full-access` | Ordinary prompts and Reasonix process sandboxing are skipped; explicit host deny rules still run before launch | Waits for user | Waits for user |
561 | Approved-plan execution window | The approved plan may execute only within the active preset; explicit `ask` / `deny` rules remain | Future plans still wait | Waits for user |
562
563 Out of the box, new sessions use `workspace-write`: workspace and private
564 session-temp operations run in the OS sandbox without prompting. Use
565 `--permission-mode read-only` for inspection-only automation or explicitly
566 select `--permission-mode danger-full-access` when unrestricted local access is
567 required. Explicit `ask` rules still require authorization, and `deny` rules
568 harden every preset.
569
570 ### 3.8 Slash commands (`internal/command`)
571
572 The chat TUI accepts `/command` input. Three kinds share one dispatch:
573
574 - **Built-in actions** (`/compact`, `/new`, `/clear`, `/effort`, `/mcp`, `/help`) manipulate session
575 state locally and never reach the model. `/new` starts a new session while
576 saving the previous transcript for resume/history. `/clear` requires
577 confirmation, then discards the current context without saving it; it does not
578 delete project memory.
579 - **Custom commands** are Markdown files under `.reasonix/commands/` (project) and
580 the user config dir, e.g. `~/.reasonix/commands/` on macOS/Linux; the project dir overrides the user dir on a
581 name clash. A file `review.md` becomes `/review`; a subdirectory namespaces it
582 (`git/commit.md` → `/git:commit`). Invoking one renders its body and sends the
583 result as the next user turn.
584 - **MCP prompts** (§3.3) appear as `/mcp__<server>__<prompt>`.
585
586 ```markdown
587 ---
588 description: Review the staged diff
589 argument-hint: [focus-area]
590 ---
591 Review the staged diff. Focus on $ARGUMENTS, list bugs with file:line.
592 ```
593
594 - Frontmatter is an optional `---`-fenced block of simple `key: value` lines;
595 `description` and `argument-hint` are recognised (no YAML dependency — Reasonix
596 stays lean). The remainder is the body template.
597 - Substitution in the body: `$ARGUMENTS` (all args, space-joined), `$1`…`$N`
598 (positional, empty when absent), `$$` (a literal `$`). Arguments are the
599 space-separated tokens after the command.
600 - Loading is pure (`command.Load(dirs...)`) and tested; a malformed file is
601 skipped, not fatal. Custom and MCP-prompt commands both resolve to text and
602 reuse the same "start a turn" path as a typed message.
603
604 #### CLI modal/composer ownership
605
606 The Bubble Tea chat TUI has one bottom composer. A slash-command overlay must
607 declare whether it owns keyboard input:
608
609 - **Modal overlays** own navigation/confirm/cancel keys and must hide the
610 composer while open. Examples: `/mcp`, `/resume`, `/rewind`, approval prompts,
611 and non-typing `ask` choice cards.
612 - **Input-owned overlays** are attached to the textarea and must keep the
613 composer visible. Examples: slash/@ autocomplete and `ask` free-text mode.
614
615 New CLI overlays must update `chat_tui.hideComposer()` and add/extend layout
616 tests so `bottomRows()` accounts for either `panel + status` or
617 `panel + composer + status`. This prevents inactive chat input boxes from being
618 rendered under modal panels.
619
620 ### 3.9 Chat references (`@`)
621
622 A chat message may embed `@` references; before the turn is sent, each is
623 resolved and prepended to the message as a tagged block the model can read.
624
625 - `@<server>:<uri>` where `<server>` is a connected MCP server → an MCP
626 resource (`resources/read`), wrapped `<resource ref="…">…</resource>`.
627 - `@<path>` otherwise → a **local file or directory**, but only when the path
628 actually exists on disk. This existence gate is the disambiguator: an ordinary
629 `@mention` or an email address resolves to no file and stays literal text. A
630 file is wrapped `<file path="…">…</file>` (size-capped, binary files noted not
631 dumped); a directory becomes a recursive listing (depth-first, skipping common
632 noise like `.git` and `node_modules`).
633 - Resolution is asynchronous (off the TUI event loop); a fetch failure surfaces
634 as a notice but doesn't block the turn. Reads are user-initiated and read-only
635 — they do **not** pass the permission gate (§3.7).
636 - Typing `/` or `@` opens an autocomplete menu above the input. The `@` menu
637 navigates **one directory level at a time** (`os.ReadDir`, never a recursive
638 walk — bounded for huge directories): a directory entry descends, a file
639 completes, and MCP resources appear alongside top-level entries. The
640 bottom-region menu changes height only on these discrete actions, never per
641 streamed token, so scrollback stays clean (§ rendering).
642
643 ### 3.10 Subagent profiles and explicit CLI execution
644
645 A subagent profile is a Skill with `runAs: subagent` and, for profiles managed
646 by the desktop or CLI editors, `invocation: manual`. Profiles reuse the existing
647 project/global Skill files; they do not introduce another state format or
648 database. Manual invocation excludes a profile from the `session-context`
649 Skills catalog so
650 the model cannot discover it implicitly, while explicit `/<name> <task>`
651 invocation remains available.
652
653 Interactive slash invocation and `Controller.RunSubagentProfile` both execute
654 the profile with the Boot-wired Skill runners. Each run gets an isolated child
655 session and returns only its final answer to the caller. The headless contract is
656 explicit:
657
658 - `reasonix subagent try <name> ... <task>` uses the read-only Skill runner;
659 - `reasonix subagent run <name> ... <task>` uses the normal permission and
660 sandbox path; and
661 - ordinary `Controller.Run` / `reasonix run` remains unchanged and does not
662 reinterpret slash-prefixed input as a subagent command.
663
664 Desktop and CLI profile mutations share
665 `skill.ValidateEditableSubagentProfile`. Only simple manual project/global
666 profiles can be rewritten or deleted. Custom-scope Skills, unmanaged
667 frontmatter, and Skill directories containing `references/` or `scripts/` are
668 refused so an editor cannot silently flatten or discard rich Skill content.
669 Built-in profiles support configuration overrides but have no writable file.
670
671 Effective model and effort precedence is: per-profile
672 `agent.subagent_models` / `agent.subagent_efforts`, this call's `model` /
673 `effort` on `task`/`fleet`, profile frontmatter, `agent.subagent_model` /
674 `agent.subagent_effort`, then executor/default model configuration.
675
676 `task` accepts optional `profile` and `write_paths`. `fleet` dispatches 2–64
677 profile-aware tasks under a session scheduler
678 (`agent.max_subagent_concurrency`, default 6; `agent.max_parallel_writers`,
679 default 3). Profile names are resolved at runtime from the Skill store and
680 must never enter tool schemas or the parent system prompt. Custom and named
681 built-in profile bodies are the full child system prompt (no implicit
682 concise default). `parallel_tasks` remains the compatible read-only batch
683 API on the same scheduler. In a persisted parent session, parallel/fleet
684 children save independent transcripts; the aggregate carries bounded previews
685 and stable refs, and `read_subagent_result` pages a referenced final answer by
686 UTF-8 byte offset under the current conversation-lineage/workspace boundary.
687 Headless runs remain ephemeral and return fair bounded previews without refs.
688 See [Subagent profiles](./SUBAGENT_PROFILES.md)
689 for the user-facing command and file-format contract.
690
691 A profile describes a worker, not a run. Delegation is five separate concepts:
692 the profile says how a worker thinks, `TaskSpec` what this call wants,
693 `CapabilityGrant` what it may touch, `ContextCapsule` what it starts from, and
694 `SchedulerPolicy` when it runs. A field belongs to whichever member decides its
695 value, so a profile may carry a capability *ceiling* (`allowed-tools`,
696 `read-only`) but never a per-call value such as `max_turns`, `write_paths`, or a
697 retry or verification policy — those are decided by the task or the scheduler.
698 Skill frontmatter may keep growing; `agent.ProfileFromSkill` is the single
699 narrowing point, and routing metadata (triggers, auto-use, cost, freshness)
700 stops there because it decides *when* a worker is chosen, not how it thinks.
701 `internal/agent/profile_boundary_test.go` fails on any widening.
702
703 ### 3.11 Sub-agents close with a host-adjudicated claim
704
705 A writer sub-agent ends its run by calling `complete_subtask` with a `status`,
706 a `summary`, the `acceptance_criteria` it was held to (each with the command it
707 ran or the paths it changed), and whatever it left `unresolved`. Prose alone is
708 still accepted, but it is no longer the interface the parent reasons over.
709
710 The submitted status is a claim, not a verdict. Before the parent sees it, the
711 host checks every citation against its own receipts: a `verification` criterion
712 must name a command the host recorded as run, `diff`/`files` must name paths the
713 host observed written or read, and a `manual` note is never self-backing. Any
714 criterion the receipts cannot back is lowered to `unsatisfied`, a report holding
715 one cannot stay `complete`, and the downgrade is printed with its reason. The
716 host never raises a status.
717
718 The parent therefore receives, in order: the adjudicated status and criteria,
719 the child's own prose, and the host's own receipts of what it changed and ran.
720
721 ### 3.12 Write claims are enforced, not advisory
722
723 A declared `write_paths` is one truth source used for both scheduling and
724 enforcement. When a writer sub-agent declares explicit paths, the host binds its
725 registry to that claim before the child runs:
726
727 - path-aware built-in writers (`write_file`, `edit_file`, `multi_edit`,
728 `move_file`, `notebook_edit`, `delete_range`, `delete_symbol`) reject any
729 argument path outside the claim, with both ends of a `move_file` checked;
730 - paths are compared after symlink resolution against the deepest existing
731 ancestor, so neither `..` traversal nor a symlink inside the claim can launder
732 a write out of it;
733 - `bash` is kept only if the OS sandbox can rebind its write roots to the claim,
734 and is otherwise removed from the child's registry entirely;
735 - MCP goes through `use_capability`, which refuses at resolve time — before any
736 MCP process runs — every target not proven read-only;
737 - writers the host cannot path-scope (custom, unknown) are dropped;
738 - after the run, the host compares the mutations it recorded against the claim
739 and reports any outside path to the parent in the sub-agent's host receipts.
740
741 Omitting `write_paths` is not an unscoped writer: the run starts by claiming
742 the whole workspace, so it cannot start beside another writer. After it has
743 only performed path-bound writes, the scheduler reservation shrinks to those
744 files and a parent (or sibling) may write elsewhere. A `bash` or MCP workspace
745 mutation makes the claim whole-workspace again. Directory claims may start
746 together; they serialize only when they realize the same file. Enforcement
747 still uses the declared bound — sandbox/`AllowsPath` do not shrink. Writes
748 that leave the workspace are still reported as claim violations.
749
750 Declaring paths is what buys parallelism; it costs `bash` on hosts where the OS
751 sandbox cannot enforce write roots.
752
753 ### 3.13 Sub-agent context inheritance is explicit
754
755 A child inherits nothing implicitly. What it receives is exactly this:
756
757 | Given to the child | Where it comes from |
758 | --- | --- |
759 | System prompt | `DefaultTaskSystemPrompt`, `DefaultReadOnlyTaskSystemPrompt`, or the profile body — nothing else is composed into it |
760 | Workspace root | `<workspace-context>` on the first user turn |
761 | The task text | the user turn itself |
762 | Completion contract | appended to a writer's task turn (§3.11) |
763 | Delegation guidance | `<subagent-context>` on a nested child's fresh session |
764 | Plan-mode marker, reasoning/response language | run options, when set |
765 | A prior transcript | only via `continue_from` / `fork_from` |
766
767 Not inherited, by construction: `REASONIX.md`, `AGENTS.md`, `CLAUDE.md`, project
768 and global memory (the memory queue is disabled, so a child cannot record memory
769 either), the parent conversation, the current Goal, planner output, and sibling
770 sub-agent results. A constraint that must reach a child today has to be in its
771 profile body or in the task text — there is no ambient channel.
772
773 Every run records a `ContextCapsule` in its transcript sidecar: the workspace,
774 the system-prompt source and hash, the resolved tool scope and schema hash, the
775 model and effort, the parent session and tool-call id, any resumed transcript,
776 and an `inherited` block whose fields are all false. `capsuleHash` is its stable
777 identity, so *why did this reviewer not see that constraint* is answered from
778 the record, and two runs that behaved differently can be diffed instead of
779 guessed at. The capsule holds references and digests only — never copied parent
780 context, which is what keeps delegation cheap and the child prefix cacheable.
781
782 ### 3.14 Fleet is a small dependency graph
783
784 A fleet item may declare `id` and `depends_on`. That is the whole graph
785 vocabulary: no conditions, no expressions, no dynamic fan-out. It is enough for
786
787 ```
788 research ──▶ implement backend ──┐
789 └──▶ implement frontend ─┴──▶ integration test ──▶ review
790 ```
791
792 Ids default to the 1-based position. A duplicate id, an id no task declares, a
793 self-edge, or a cycle fails preflight, so a fleet that cannot finish never
794 starts. Items run as soon as their dependencies complete; items with no ordering
795 between them run in parallel under the same session scheduler as before.
796
797 Dependencies are a property of the graph, never of a task: they live in the
798 fleet plan and never reach `ProfileExecSpec`, which is what keeps `depends_on`
799 from becoming the first keyword of a workflow language.
800
801 The graph relaxes the write-claim preflight in the one place it should. Only
802 items that can run at the same time need disjoint `write_paths`; an
803 `implement → review` pair is serialised by its edge and may share paths, which
804 a flat fleet could not express.
805
806 Failure handling has one knob. A failed or skipped task always skips its whole
807 downstream branch — running a dependent on a broken input only buys a result the
808 parent must discard. Independent branches keep going unless `fail_fast` is set,
809 which stops *starting* new tasks; tasks already running are left to finish so a
810 writer is never abandoned mid-write.
811
812 ### 3.15 One child-construction primitive
813
814 The APIs that spawn a child are many — `task`, `read_only_task`, `fleet`,
815 `parallel_tasks`, `run_skill`, `/<profile>`, `reasonix subagent run|try`,
816 desktop preview. The execution primitive behind them must stay one. Each entry
817 point compiles its request into a `ProfileExecSpec` and hands it to
818 `TaskTool.RunProfileSpec`, which is the only place that resolves depth, tool
819 scope, permissions, sandbox, write claims, scheduler slots, the MCP frontend,
820 the transcript and capsule, the evidence ledger, and the completion contract.
821
822 This is not a style preference. A safety boundary spread across several
823 construction paths only has to be forgotten once: past regressions where a
824 preview path built unconfined file tools, and where a profile editor dropped
825 `read-only` on save, were both one entry point missing one layer.
826
827 An entry point that must not persist a transcript says so with
828 `ContextRequest.Ephemeral` rather than building its own session, so its promise
829 is a field on the spec instead of a second construction path.
830
831 `internal/agent/spawn_boundary_test.go` enumerates the files that still call the
832 low-level runners directly and fails on any new one. The remaining entries —
833 `internal/boot` (skill runners), `internal/cli/review.go`, and
834 `desktop/subagents_app.go` — are known debt, not precedent.
835
836 ### 3.16 MCP concurrency: read-only is not stateless
837
838 Sub-agents share one session Host and its connections while each keeps its own
839 `use_capability` frontend and ledger. For a stdio server that means they share
840 one process, and therefore its session state.
841
842 Read-only does not imply stateless. A browser server opens a page, selects a
843 tab, scrolls; every one of those tools may honestly declare `readOnly` because
844 nothing reaches the filesystem, yet two children calling it concurrently
845 interleave on state neither of them can see. Write claims do not help — there is
846 nothing to claim.
847
848 A configured server therefore carries a concurrency policy:
849
850 ```toml
851 [[mcp.servers]]
852 name = "browser"
853 concurrency = "serial" # parallel (default) | serial
854 ```
855
856 `serial` means the runtime never runs two calls to that server at once across
857 the whole session, whichever child issues them. The gate lives on the shared
858 runtime because the process being interleaved on is shared at exactly that
859 scope, and a call waiting on it still honours its own cancellation. Servers
860 whose names look known-stateful (browser, playwright, puppeteer, chrome,
861 chromium, selenium) default to `serial`; explicit configuration always wins, and
862 everything else stays parallel so the shared-Host tradeoff is unchanged.
863
864 This is deliberately the conservative first version: one policy per server, not
865 per capability. Per-tool `parallel_safe` / `exclusive` hints and explicit
866 `concurrency_key` grouping are the later refinement, once real servers show
867 which tools within one server genuinely differ.
868
869 ### 3.17 Measuring whether delegation pays
870
871 Orchestration is easy to add and hard to justify: more agents always cost more
872 tokens, and the extra tokens alone can look like an improvement. Comparing arms
873 therefore has to hold the model fixed and read host-recorded facts, not prose.
874
875 `reasonix run --json` emits per-run delegation counters alongside the existing
876 token, cache, cost, and duration totals:
877
878 | Counter | Answers |
879 | --- | --- |
880 | `subagent_runs`, `subagent_nested_runs` | which shape actually ran, not which was configured |
881 | `tool_calls` − `subagent_tool_calls` | parent versus child work split |
882 | `subagent_mutations`, `duplicate_work_paths` | did two children redo the same file |
883 | `completion_reports`, `completions_prose_only` | how much of the run ended in a checkable claim |
884 | `false_completions`, `criterion_downgrades` | claims the host refused to back |
885 | `write_scope_violations` | writes that escaped a declared claim |
886
887 The control axis is partial, and the counters are what revealed it.
888 `--ablate subagent` removes `task`, `read_only_task`, `fleet`, and
889 `parallel_tasks`, but a run can still delegate through a `runAs=subagent`
890 profile skill: a measured `no-subagent` arm spent a child run on `explore`.
891 Treat that arm as "no task-tool delegation", not "single agent", and read
892 `subagent_runs` to see what actually happened rather than trusting the label.
893 Nested depth is `agent.max_subagent_depth`.
894
895 `false_completions` is the counter that matters most. It comes from the
896 adjudication in §3.11, so it measures claims the host refused rather than a
897 reviewer's opinion, and it is the one number that separates "the fleet finished
898 faster" from "the fleet said it finished".
899
900 Read these against the measured noise floor. Running the same arm twice over
901 the same tasks moved per-task token use by a median of 19% and up to 54%, while
902 the whole between-arm difference in that experiment was 2.5%. A single run per
903 cell therefore proves nothing about delegation: the effect has to clear the
904 variance before it is an effect. Budget repetitions, or restrict the comparison
905 to tasks where `subagent_runs` shows delegation actually happened — in that
906 experiment it happened on one task in six.
907
908 What the counters have measured so far, on one model over four task shapes,
909 each comparing a neutral prompt against a forced-delegation twin over identical
910 work: three one-line fixes in separate modules cost 3.8x the tokens; a 24-file
911 search 1.5x tokens and 2.2x wall; a 36-file three-package migration 2.6x tokens
912 and 4.1x wall; three genuinely heterogeneous branches, the shape with the best
913 theoretical case, 2.4x tokens and 3.7x wall over three repetitions. Success rate
914 was 100% everywhere, and the forced arm's spread was about twice the neutral
915 arm's, so delegation also buys variance.
916
917 Read a child's token figure carefully: 27 measured child runs averaged 134k
918 tokens each, but that is cumulative prompt tokens over 9.3 model calls with the
919 same ~14k context re-sent each time, not 134k tokens of new material. At ~90%
920 cache hit the real price of a child averaged ¥0.017. The 2-4x above is the
921 number that matters, because both arms are counted the same way; the per-child
922 total is not a threshold to compare a branch's size against.
923
924 Why delegation is rare is answerable from the same runs, and the answer is not
925 that the model weighs it and declines. Across 33 runs with delegation available,
926 15% delegated and bash outnumbered every delegation-class call 10:1. The
927 recorded reasoning shows the model deliberating over how to read efficiently —
928 "that's 25 files... read them in parallel batches... I can read multiple files
929 at once" — on a task built for `explore`, without delegation entering the
930 decision at all.
931
932 Three things explain that, and only one of them is a defect. The base system
933 prompt never mentions delegation; every mention lives in the skills index, and
934 each is a brake ("the heavy path... only when the task genuinely needs
935 context-heavy work, not on weak relevance") next to an accelerator for inline
936 skills ("even plausibly relevant... cheap"). The `task` tool description says
937 what the tool does and never when to reach for it. And the model already has
938 cheaper parallelism — several tool calls in one round trip, with no context
939 duplicated — which is what it reasons in terms of.
940
941 Given the measured 2.4-4.5x, a brake is the correct default; the gap is that
942 nothing recognises the rare case where delegation would pay. Forcing it does not
943 close that gap: in the forced fleet run the parent worked out all three fixes in
944 its own reasoning before dispatching, so the children re-read the code to apply
945 edits the parent had already derived. Delegation moved the typing, not the
946 thinking.
947
948 One hypothesis remains untested rather than disproved: delegation's isolation
949 should pay when the parent is actually hurt by what it read. It could not be
950 provoked here. Pinning a workspace `compact_ratio` down to 0.5% still produced
951 zero compactions, because the agent keeps its session small by writing a script
952 instead of reading — the same behaviour that wins it the comparisons. Context
953 pressure needs a task that cannot be scripted away, which this corpus does not
954 yet contain.
955
956 The migration is the instructive one. Left alone the agent read a single file,
957 wrote a script and changed 108 call sites in 28 seconds; split across three
958 packages, no branch could see the transformation that solved all three. A task
959 looking parallel-shaped is not evidence that splitting it is cheaper.
960
961 Not yet measured, and deliberately not faked: rework-after-handoff needs
962 mutation ordering across a whole run, which belongs to the harness driving the
963 arms rather than the instrument recording one.
964
965 ## 4. Data Types (`internal/provider`)
966
967 ```go
968 type Role string
969 const (RoleSystem Role = "system"; RoleUser Role = "user"
970 RoleAssistant Role = "assistant"; RoleTool Role = "tool")
971
972 type Message struct {
973 Role Role `json:"role"`
974 Content string `json:"content,omitempty"`
975 ToolCalls []ToolCall `json:"tool_calls,omitempty"`
976 ToolCallID string `json:"tool_call_id,omitempty"`
977 Name string `json:"name,omitempty"`
978 }
979
980 type ToolCall struct { ID, Name, Arguments string } // Arguments: raw JSON
981 type ToolSchema struct { Name, Description string; Parameters json.RawMessage }
982 type Request struct { Messages []Message; Tools []ToolSchema; Temperature float64; MaxTokens int }
983
984 type ChunkType int
985 const (ChunkText ChunkType = iota; ChunkToolCall; ChunkDone; ChunkError)
986
987 type Chunk struct {
988 Type ChunkType
989 Text string // ChunkText
990 ToolCall *ToolCall // ChunkToolCall
991 Err error // ChunkError
992 }
993 ```
994
995 ## 5. Configuration (TOML)
996
997 Resolution order: **flag > project `./reasonix.toml` > the user config file
998 > built-in defaults**. Starting with **Reasonix v1.8.1**, the user config lives
999 at `~/.reasonix/config.toml` on macOS/Linux and
1000 `%AppData%\reasonix\config.toml` on Windows. See
1001 [Configuration paths](./CONFIG_PATHS.md) for migration and related data paths.
1002 Fields marked user/global only are not overridden by project `reasonix.toml`.
1003 Provider entries name secrets with `api_key_env`; saved key values live in
1004 Reasonix's global `<Reasonix home>/.env`, shared by CLI and desktop. Project
1005 `.env`, home `.env`, inherited shell environment variables, legacy credentials,
1006 and the OS keyring are not provider-key runtime fallbacks. Project `.env` still
1007 feeds workspace-scoped, non-provider `${VAR}` expansion for MCP/plugin settings
1008 without importing provider keys or Reasonix control variables.
1009
1010 ```toml
1011 default_model = "deepseek" # provider name (→ its default model) or "provider/model"
1012 # language = "zh" # ui language tag; empty = auto-detect from $LANG / $REASONIX_LANG
1013
1014 [ui]
1015 # shortcut_layout = "desktop" # classic|desktop; compatibility setting
1016 # cursor_shape = "bar" # CLI/TUI textarea cursor: underline|block|bar
1017 show_turn_usage = false # hide per-request token/cost receipts in the TUI; default true
1018
1019 [agent]
1020 system_prompt = "You are Reasonix, a coding agent..." # or system_prompt_file = "..."
1021 temperature = 0.0
1022 reasoning_language = "auto" # visible reasoning text: auto|zh|en
1023 # plan_mode_read_only_commands = ["gh issue view"] # legacy compatibility only; Plan bash uses Permissions
1024 # planner_model = "deepseek-pro" # optional: two-model collaboration (low-frequency planner)
1025 # subagent_model = "deepseek-pro" # optional default for runAs=subagent skills
1026 # subagent_effort = "high" # optional default reasoning effort for subagents
1027 # subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" }
1028 # subagent_efforts = { review = "max", security_review = "high" }
1029
1030 # A vendor endpoint exposing several models under one base_url/key.
1031 [[providers]]
1032 name = "deepseek"
1033 kind = "anthropic"
1034 base_url = "https://api.deepseek.com/anthropic"
1035 # request_url = "https://proxy.example.com/anthropic/v1/messages" # optional exact provider request URL
1036 # models_url = "https://proxy.example.com/v1/models" # optional model discovery URL
1037 models = ["deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"]
1038 default = "deepseek-v4-flash" # optional; defaults to models[0]
1039 # vision_models = ["deepseek-v4-flash-vision-exp"] # legacy compatibility; Settings derives image support from model metadata
1040 # Official DeepSeek vision accepts inline base64, http(s) image URLs, and Files API file_id.
1041 api_key_env = "DEEPSEEK_API_KEY"
1042 web_search = true
1043 context_window = 1000000 # tokens; harness compacts older history near this limit (0 disables)
1044 # max_output_tokens = 0 # auto: provider capability; official DeepSeek omits until the window is tight
1045 # max_output_tokens = 32768 # optional cost cap; still clipped to physical remaining
1046 # max_output_tokens = 65536 # optional cost cap
1047 # max_output_tokens = -1 # force-omit optional wire limits; compact if the auto budget no longer fits
1048 # max_output_tokens never changes compact_ratio
1049 # model_overrides = { "deepseek-v4-flash" = { context_window = 1000000, max_output_tokens = 32768 } }
1050
1051 # A single-model entry still works for custom OpenAI-compatible endpoints.
1052
1053 [environment]
1054 enabled = true # inject a stable startup summary of OS, shell, and common tool versions
1055 offline = false # set true when outbound network access is unavailable; prevents futile retries
1056
1057 # Optional trusted executable paths shown to the model when PATH probing is not enough.
1058 # Workspace-local paths are listed but not auto-executed during startup probing.
1059 # [environment.tools]
1060 # go = "/opt/homebrew/bin/go"
1061
1062 [tools]
1063 enabled = [] # omit/empty = all built-ins
1064 bash_timeout_seconds = 120 # foreground safety cap; set 0 for no tool-local cap
1065 mcp_startup_timeout_seconds = 30 # background initialize + tools/list safety cap
1066 mcp_call_timeout_seconds = 300 # default MCP call safety cap; plugin/tool overrides may raise it
1067
1068 [tools.shell]
1069 prefer = "auto" # auto (default) | bash | powershell | pwsh — force the shell tool's interpreter
1070 # path = "C:\\Program Files\\PowerShell\\7\\pwsh.exe" # explicit executable for the chosen shell
1071
1072 [skills]
1073 # paths = ["~/my-skills", "../shared/skills"] # extra custom skill roots
1074 # excluded_paths = ["~/.agents/skills"] # hide convention roots without deleting folders
1075 # disabled_skills = ["review"] # hidden from prompt, slash invocation, and skill tools
1076
1077 [permissions]
1078 mode = "ask" # writer fallback when no rule matches: ask|allow|deny
1079 deny = ["Bash(rm -rf*)", "Bash(git push*)"] # hard-blocked in every mode
1080 allow = ["Bash(go test:*)", "Bash(git status:*)"] # never prompted
1081 ask = [] # force a prompt even if otherwise allowed
1082
1083 [sandbox]
1084 # workspace_root = "" # file-writers confined here; empty = cwd
1085 # allow_write = ["/tmp"] # extra dirs write_file/edit_file/multi_edit/move_file may modify
1086 # forbid_read = ["${HOME}/.ssh"] # paths read/list/search tools and sandboxed bash may not inspect
1087
1088 [serve]
1089 auth_mode = "none" # none|token|password; use auth before binding beyond localhost
1090 # token = "" # optional fixed token; empty token mode generates one at startup
1091 # password_hash = "" # bcrypt hash generated with reasonix serve --hash-password --password '...'
1092 # behind_proxy = false # trust X-Forwarded-* only behind a trusted reverse proxy
1093
1094 [[plugins]]
1095 name = "example" # type defaults to "stdio"
1096 command = "reasonix-plugin-example"
1097 args = []
1098 # env = { FOO = "bar" }
1099 # startup_timeout_seconds = 60 # initialize + tools/list cap; 0 = global/default cap
1100 # call_timeout_seconds = 600 # per-server MCP call timeout; 0 = global/default cap
1101 # tool_timeout_seconds = { "generate_video" = 1800 } # raw MCP tool names
1102 # [[plugins]] # a remote MCP server over Streamable HTTP
1103 # name = "stripe"
1104 # type = "http" # "stdio" (default) | "http" | "sse"
1105 # url = "https://mcp.stripe.com"
1106 # headers = { Authorization = "Bearer ${STRIPE_KEY}" } # ${VAR} / ${VAR:-default} expanded
1107 ```
1108
1109 The native CLI updater always installs the latest strict `vX.Y.Z` official
1110 release. Legacy channel configuration and arguments remain parseable during
1111 1.x, resolve to the official release, and are omitted on subsequent writes.
1112
1113 The executor tracks an adaptive progress lease while a todo is active. A new
1114 completion, unique successful read, command, or mutation renews the lease;
1115 exact repeats do not. After 8 no-progress tool-call rounds the host appends a
1116 one-shot reassessment nudge. In Goal mode, the later threshold forces a re-plan
1117 and continues; outside Goal it may end the current attempt. The serial contract is level-aware while preserving the
1118 single-in_progress rule: in a two-level list the active level-1 sub-step is
1119 the only `in_progress` item and its level-0 phase stays `pending`; sub-steps
1120 complete in order, and the phase becomes `in_progress` — and signs off — only
1121 after all of its sub-steps have completed. A level-1 item with no phase above
1122 it is rejected. Retired `[agent].max_steps` and `planner_max_steps` keys remain
1123 parseable for upgrade compatibility, but are ignored and removed by a one-time
1124 migration. The CLI `--max-steps` flag and `[bot].max_steps` remain separate,
1125 explicit controls for one-off and unattended execution; bot `0` means continuous.
1126
1127 `reasonix setup` writes this default config so the CLI is usable out of the box.
1128
1129 `[ui].cursor_shape` is normalized to `underline`, `block`, or `bar`; empty or
1130 unknown values fall back to `bar`. It applies to the Bubble Tea CLI/TUI
1131 textarea only, while desktop and browser inputs keep their platform-native
1132 cursor behavior.
1133
1134 `[serve]` controls the HTTP browser frontend used by `reasonix serve`. The
1135 default `auth_mode = "none"` is intended for the loopback default
1136 `127.0.0.1:8787`; deployments reachable from another machine must use `token` or
1137 `password`. Password mode requires either a startup `--password` or a stored
1138 bcrypt `password_hash`. `behind_proxy` must stay false unless the server is
1139 behind a trusted proxy that owns the `X-Forwarded-For` and `X-Forwarded-Proto`
1140 headers.
1141
1142 MCP servers may also be declared in a project-root `.mcp.json` using Claude
1143 Code's exact `mcpServers` schema (`command`/`args`/`env`, `type`/`url`/`headers`,
1144 `${VAR}` expansion). It is read after the TOML files and merged into
1145 `[[plugins]]`; on a name collision `reasonix.toml` wins (it is the more explicit,
1146 Reasonix-specific source). This lets a server already configured for Claude work in
1147 Reasonix unchanged.
1148
1149 MCP startup has a separate lifecycle from an individual tool call. A caller
1150 waits briefly for cold startup, while the shared launch/authorization/
1151 `initialize`/`tools/list` sequence may continue in the background up to
1152 `mcp_startup_timeout_seconds` (default `30`). A per-server
1153 `startup_timeout_seconds` overrides that cap. MCP call timeouts begin only after
1154 the connection is ready.
1155
1156 ```json
1157 { "mcpServers": {
1158 "stripe": { "type": "http", "url": "https://mcp.stripe.com",
1159 "headers": { "Authorization": "Bearer ${STRIPE_KEY}" } }
1160 } }
1161 ```
1162
1163 `[sandbox]` is the *enforcement* layer beneath permissions (which are *policy*).
1164 They stay two layers: a permitted call still cannot write outside the approved
1165 roots. Interactive sessions can extend those roots with a write-access approval
1166 (once / session / project `reasonix.toml` / deny). File tools request the target
1167 parent directory automatically. Bash must declare `additional_write_dirs` and a
1168 `justification`; the host does not infer paths from the command text. Headless
1169 `reasonix run` fails closed unless the directory is already in
1170 `[sandbox].allow_write` or `--add-dir`. Granting `${HOME}` is allowed with a
1171 high-risk warning; the filesystem root and Reasonix session/state paths are not.
1172 Phase 0 confines the file-writing built-ins (`write_file`, `edit_file`,
1173 `multi_edit`, `move_file`) to `workspace_root` (default cwd), the Reasonix user
1174 config dir, plus `allow_write`: a write whose target — resolved to an absolute,
1175 symlink-free path so a symlinked dir or `..` cannot tunnel out — falls outside
1176 every root is refused, and the error is fed back to the model. Confinement is on
1177 by default (root = cwd), so edits stay in the project while the agent can still
1178 update its own global config. `forbid_read` lists files or directories the agent should
1179 not read, list, or search; entries support `${VAR}` / `${VAR:-default}` expansion
1180 and should be absolute, or use `${HOME}` for home-relative secrets such as
1181 `${HOME}/.ssh`. `bash` is itself jailed by default when an OS sandbox is
1182 available (`[sandbox] bash = "enforce"`: Seatbelt on macOS and bubblewrap on
1183 Linux): each command is allowed to write only
1184 the same roots plus platform-specific command temp/cache roots, denied reads
1185 under `forbid_read`, and allowed to reach the network only when
1186 `network = true`.
1187 **Windows status:** Reasonix does not ship an OS-level Bash sandbox on Windows.
1188 The effective mode is fixed to `off`; an older config containing
1189 `bash = "enforce"` remains readable but resolves to `off`, `reasonix doctor`
1190 reports the ignored value, and the desktop control is read-only. Bash therefore
1191 runs unconfined on Windows. The in-process file tools continue to enforce
1192 `workspace_root`, `allow_write`, and `forbid_read`.
1193 When no OS sandbox is available, `bash = "enforce"` refuses bash execution
1194 instead of running unconfined. Install the platform sandbox backend
1195 (bubblewrap/`bwrap` on Linux, `sandbox-exec` on macOS) or set
1196 `[sandbox] bash = "off"` to explicitly restore the pre-1.16 unconfined shell
1197 behavior. The escape-prompt and broader OS support are Phase 1's remainder (§9).
1198
1199 ## 6. Error Handling
1200
1201 - Library code wraps with `fmt.Errorf("...: %w", err)` and returns; it never
1202 prints or calls `os.Exit`.
1203 - Only `cli` / `main` decide exit codes and user-facing messages.
1204 - Tool execution errors are fed back to the model, not fatal.
1205 - Network layer should apply bounded exponential backoff on 429 / 5xx
1206 (interface reserved; implementation may follow).
1207
1208 ## 7. Code Style
1209
1210 - `gofmt` + `go vet` must be clean; package names lowercase; exported
1211 identifiers documented; comments explain *why*, not *what*.
1212 - No premature generalization. Prefer clear and direct.
1213
1214 ## 8. Distribution
1215
1216 - Build: `CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$(VERSION)" -o reasonix ./cmd/reasonix`
1217 - Cross matrix: `darwin|linux|windows` × `amd64|arm64`.
1218 - Version injected via ldflags (`git describe --tags --always`).
1219 - Install: prebuilt binary / `go install` / future `brew tap`.
1220
1221 ## 9. Roadmap (not in current scope)
1222
1223 - Sandbox Phase 1: an OS-level jail for `bash` so commands — not just the
1224 file-writer built-ins (Phase 0) — are confined to the workspace. **Seatbelt on
1225 macOS and bubblewrap on Linux ship, on by default when available** (see §5).
1226 Restricted presets fail closed when the platform sandbox cannot be established;
1227 Reasonix never offers an unconfined retry as a fallback.
1228 - MCP long tail (deferred deliberately): `headersHelper` auth for remote
1229 servers; the remaining `.mcp.json` scopes
1230 (local / user — project scope shipped, see §5); tool-search deferral;
1231 `list_changed` live updates; channels / elicitation / roots; plugins that
1232 provide *providers*, not just tools.
1233 - An Anthropic-native provider `kind` (native prompt-cache control), proving the
1234 registry generalises beyond one wire format.
1235
1235 lines MARKDOWN