The proposal survives contact with the code with three amendments, per
the RFCs-are-proposals rule. (1) A mode transition logs a
request/header-delta only when expressible: adding exit_plan_mode
resorts the canonical tool list, and a pure reordering has no delta
form, so entering plan mode logs the full fallback snapshot — the
attributability claim holds either way. (2) The proposed/ skeleton
converts to the implemented grammar: Proposal → Decision, the roadmap's
staging (now history) drops to the standing Deferred list, and
Acceptance criteria + Risks fold into Consequences (what holds, then the
accepted costs, including the ACP v2 mode-removal migration). (3) The
two recorded scenarios stay pending a with-key session, recorded in
Deferred.
The docs tail completes: the cookbook's plan-mode row upgrades from
sketch to the shipped package, architecture.md gains the ctx.modes
capability row (ceiling 1640 → 1650: a new capability service's table
row does not fit the old budget), and every reference repoints to
implemented/.
@@ -102,7 +102,7 @@ Every product feature maps to a listener on a documented extension seam — the
| ToolSearch / progressive disclosure | filter tools at `system-prompt/assemble` (the assembly carries the schemas; the loop logs the result as the request header, so disclosure stays reconstructable) |
| Tool sandbox (landlock / sandbox-exec) | `tools/pre-execute` (deny), or a sandboxing `BashExecutor` on the `dsh-bash` seam |
| Permission system / AskUserQuestion | `tools/pre-execute` (deny/ask); register an ask tool |
| Plan mode | `tools/pre-execute` (deny writes) + a mode prompt section via `ctx.systemPrompt.section()` or `agent.inject()` (model-visible ⟺ logged:`agent/request` shapes call config only) |
| Plan mode | Shipped: [`@deepseek-ai/dsh-mode`](../../packages/mode/mode/README.md) — the logged `mode/set` state, the assemble filter + `mode:policy` section, the deny-by-default `tools/pre-execute` gate, and the user-reviewed `exit_plan_mode` exit |
| Sub-agent delegation | the `ctx.subagents` provider registry (`dsh-subagent-spawn`/`-fork`/`-acp`) + `dsh-tool-subagent` exposing one configured provider to the model |
| MCP | one plugin per server: discover tools → `ctx.tools.register()` |
# RFC: Plan mode — a logged per-agent session mode
Status: proposed
Status: implemented
## Problem
@@ -8,13 +8,13 @@ The harness has no way to put an agent into a reduced-authority working state. T
Every shipped plan mode decomposes into the same five parts — a low-authority tool policy, a plan artifact, an approval moment, an execution-state switch, and durable state ([Prior art](#prior-art) carries the survey). Four of the five already exist here as gated infrastructure: what the model is TOLD it can do is shaped per step at [`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) and whatever ships is logged as `request/header*` events ([reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md)); what can RUN is gated at `tools/pre-execute` with typed decisions ([interception seams](../../implemented/feature/2026-06-30-interception-seams.md)); the approval moment is a human answer over the user-interaction seam ([`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md), the [ask-user precedent](../../implemented/feature/2026-06-25-ask-user-question.md)); durable per-agent facts are `SessionEventMap` members ([the `todo/write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)). The missing fifth is the mode itself: a named, durable, per-agent policy state the policy listeners can read.
## Proposal
## Decision
The deliverable is **plan mode**. It ships as the first **session mode** — a named, logged, per-agent policy state: mode definitions — which tools stay visible, what guidance section renders — are deployment config, and the mode IN FORCE for an agent is session state, folded from its log. One new product package, `@deepseek-ai/dsh-mode` at `packages/mode/mode/` (a new top-level group, the `packages/approval/` shape), owns the event vocabulary, a thin `ctx.modes` service, and every policy listener; the loop does not change. `plan` is the only shipped definition — the mode-shaped vocabulary exists so a second mode never renames durable event types, not because more modes ship now.
The state is one `SessionEventMap` member: **`mode/set`**, a log-only, non-surface event carrying `{ mode: string }` with whole-value-replace semantics, plus a pure `foldMode(events)` that returns the mode in force — the last `mode/set`, or the default mode when none exists. Because [the log is the fact channel](../../implemented/architecture/2026-06-30-event-domain-semantics.md), resume, fork, and compaction restore the mode with no extra machinery, and UIs read flips off `session/event`. The default mode is the absence of policy — no section, no filtering, no gate — so an agent that never sees a `mode/set` behaves byte-identically to a deployment that never loads `dsh-mode`, which keeps every existing snapshot golden stable and makes the plugin safe to compose unconditionally.
Enforcement is two layers that cover each other. The **soft** layer is a `system-prompt/assemble` listener that filters the tool schemas down to the mode's allowlist and appends the mode's guidance section — every transition therefore surfaces as an attributable `request/header-delta` on the next step, keeping the [reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) invariant green by construction. The **hard** layer is a `tools/pre-execute` listener that denies, deny-by-default against the same allowlist, any call the mode does not permit — so a hallucinated call to a still-registered tool, or a schema re-widened by a foreign assemble listener, still cannot run.
Enforcement is two layers that cover each other. The **soft** layer is a `system-prompt/assemble` listener that filters the tool schemas down to the mode's allowlist and appends the mode's guidance section — every transition therefore surfaces as an attributable `request/header` event on the next step (a delta when expressible, the full fallback snapshot otherwise), keeping the [reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) invariant green by construction. The **hard** layer is a `tools/pre-execute` listener that denies, deny-by-default against the same allowlist, any call the mode does not permit — so a hallucinated call to a still-registered tool, or a schema re-widened by a foreign assemble listener, still cannot run.
The model leaves plan mode through the **`exit_plan_mode`** tool: its single argument is the plan text, which makes the plan a durable log artifact, and the tool conducts the review itself through the user-interaction seam — a question with options and a free-text channel, not a bare permission — so an approval flips the logged mode back to the default, and a rejection becomes the corrective error carrying the user's feedback verbatim, which keeps the model planning with direction. A user flips the mode from any surface through `ctx.modes.set()`; the flip is applied at the next turn boundary (session events are turn-enclosed) and narrated to the model once, only when the model-visible state actually changed.
@@ -28,7 +28,7 @@ The model explores and designs with what remains; if it attempts a write anyway,
When ready, the model calls `exit_plan_mode` with the plan markdown as its argument; the UI renders the plan as the call card and the review question arrives through the user-interaction channel — approve, or keep planning, with free-text feedback welcome — so what the human reviews is exactly the logged artifact.
On approve, the tool flips the logged mode back to the default: the next step runs with the full toolset and the widening `request/header-delta` in the log, and execution tracking from there is already `todo_write`'s job. On keep-planning, the model receives a corrective error carrying the user's feedback text, revises, and re-presents.
On approve, the tool flips the logged mode back to the default: the next step runs with the full toolset and the widening header event in the log, and execution tracking from there is already `todo_write`'s job. On keep-planning, the model receives a corrective error carrying the user's feedback text, revises, and re-presents.
### Deployment configuration
@@ -92,7 +92,7 @@ A contained `session/event` listener ([defensive patterns](../../../defensive-pa
### The soft layer: a computed section and a post-`next()` filter
A `system-prompt/assemble` waterfall listener reads the calling agent's mode (the `AssembleContext` carries `agent`) and, in a non-default mode, filters `assembly.tools` down to the mode's allowlist and appends the mode's guidance section. The loop already renders per step and logs the result: entering or leaving a mode surfaces as a `request/header-delta`on the next step, so every mode transition is an attributable, diffable log fact. The section is static per mode and the plan itself stays in the conversation (messages and tool args, already in context), so a mode does not add per-step prompt churn — re-injecting plan state into every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary and would only burn prefix cache.
A `system-prompt/assemble` waterfall listener reads the calling agent's mode (the `AssembleContext` carries `agent`) and, in a non-default mode, filters `assembly.tools` down to the mode's allowlist and appends the mode's guidance section. The loop already renders per step and logs the result: entering or leaving a mode surfaces on the next step as a `request/header-delta`— or as the full `request/header` fallback snapshot when the change is inexpressible in the delta encoding (adding `exit_plan_mode` resorts the canonical tool list, and a pure reordering has no delta form) — so every mode transition is an attributable log fact. The section is static per mode and the plan itself stays in the conversation (messages and tool args, already in context), so a mode does not add per-step prompt churn — re-injecting plan state into every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary and would only burn prefix cache.
The guidance section is an ordinary registered section, `{ name: 'mode:policy', order: 50, text: context => … }` — order 50 sits after the persona (0) and before tool guidance (100–199); it resolves to the folded mode's configured text and to `''` (dropped at render) for the default mode or an agent-less assembly. The tool filter wraps: it awaits `next()` and filters the RETURNED assembly's `tools`, so additions made anywhere inside its wrap are covered. The filter enforces one rule in every mode: `exit_plan_mode` is visible IFF the agent's folded mode is `plan` — which is also what keeps a default-mode assembly byte-identical to a no-`dsh-mode` deployment even though the tool is always registered. In a non-default mode it additionally intersects with the mode's allowlist.
@@ -111,7 +111,7 @@ The gate folds the LOGGED mode only, never the pending intent — enforcement ju
### `exit_plan_mode`
`defineTool` with one required `plan: string` argument — the plan is thereby a durable, replayable log artifact riding the ordinary `tool/call` event. `execute` rejects an agent-less call (the [`todo_write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)), re-checks the folded mode as defense in depth, then conducts the review: one single-select `ctx.userInteraction.ask()` question — approve, or keep planning — with the free-text channel open for feedback. Approve appends `mode/set { mode: 'default' }` in-turn (a tool execution is already turn-enclosed — the `todo/write` path) and returns a short confirmation; the next step's assembly restores the full toolset and logs the widening `request/header-delta`. Every other outcome — keep-planning (the user's feedback text carried verbatim), an aborted question, a missing provider — returns the corrective `isError` that tells the model to revise and re-present, and the mode stays `plan`.
`defineTool` with one required `plan: string` argument — the plan is thereby a durable, replayable log artifact riding the ordinary `tool/call` event. `execute` rejects an agent-less call (the [`todo_write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)), re-checks the folded mode as defense in depth, then conducts the review: one single-select `ctx.userInteraction.ask()` question — approve, or keep planning — with the free-text channel open for feedback. Approve appends `mode/set { mode: 'default' }` in-turn (a tool execution is already turn-enclosed — the `todo/write` path) and returns a short confirmation; the next step's assembly restores the full toolset and logs the widening header event. Every other outcome — keep-planning (the user's feedback text carried verbatim), an aborted question, a missing provider — returns the corrective `isError` that tells the model to revise and re-present, and the mode stays `plan`.
Its [render intent](../../implemented/architecture/2026-07-02-tool-render-intent-union.md), decided up front: `presentCall` is a `generic` card titled by the plan's first heading with the plan markdown as content, plus a `generic` result card — the review question arrives beside this already-streamed card, so what the human reviews is exactly the logged artifact. The seam is consumed opportunistically (`ctx.get('userInteraction')`), so `dsh-mode` composes without it and degrades to the manual exit pinned in the [FAQ](#faq).
@@ -121,31 +121,21 @@ Its [render intent](../../implemented/architecture/2026-07-02-tool-render-intent
### The recorded scenario and the harness op
`input.json` gains one step op, `{ "op": "setMode", "modeId": "plan" }`, driven through the real `session/set_mode` RPC, and a scripted `elicitationAnswers` queue (FIFO, consumed by the harness client's elicitation callback — the review question's answer). The `plan-mode` scenario: initialize → newSession → setMode(plan) → a prompt that explores and attempts a `write` (denied by the gate, pinned verbatim) → the model presents the plan via `exit_plan_mode` → a scripted approve → a follow-up prompt that writes for real. Because the mode is set before turn 1, the FIRST `request/header` snapshot is already in plan shape (filtered tools + section, reason `initial`) — the widening delta appears at the exit; the scenario pins both, plus the `mode/set` pair. A sibling `plan-mode-reject` scenario scripts the keep-planning answer with feedback text and pins the corrective result. Both need a with-key recording session; the deny/reject texts are meanwhile pinned at the unit tier.
`input.json` gains one step op, `{ "op": "setMode", "modeId": "plan" }`, driven through the real `session/set_mode` RPC, and a scripted `elicitationAnswers` queue (FIFO, consumed by the harness client's elicitation callback — the review question's answer). The `plan-mode` scenario: initialize → newSession → setMode(plan) → a prompt that explores and attempts a `write` (denied by the gate, pinned verbatim) → the model presents the plan via `exit_plan_mode` → a scripted approve → a follow-up prompt that writes for real. Because the mode is set before turn 1, the FIRST `request/header` snapshot is already in plan shape (filtered tools + section, reason `initial`) — the widening header event appears at the exit; the scenario pins both, plus the `mode/set` pair. A sibling `plan-mode-reject` scenario scripts the keep-planning answer with feedback text and pins the corrective result. Both need a with-key recording session; the deny/reject texts are meanwhile pinned at the unit tier.
### The mechanical tail
No new cordis event is declared (`mode/set` rides `session/event`; the listeners attach to existing waterfalls), so the events catalog is untouched; regenerated in the same change: the persistence log catalog (`mode/set`), the services catalog (`ctx.modes`, JSDoc-complete), the config catalog (`ModeConfig`), the tool catalog (`exit_plan_mode`), the producer/consumer map and doc graphs, and the module graph. Repo plumbing: a root tsconfig `paths` entry, the new group's README plus a [packages map](../../../../packages/README.md) row (a new top-level group is the deliberate act that table names), an `architecture.md` capability-services row for `ctx.modes` (budget-checked), and the cookbook row upgrade.
## Roadmap
## Deferred
Plan mode is one feature and lands as one. An agent that can be locked into planning but has no sanctioned way to propose leaving it is not a smaller version of the feature — it is a different and worse one, where every plan ends with the model asking the user to flip a switch it cannot see. The two stages below are therefore build-and-review order for one stacked landing ([stacked-review guide](../../../cookbook/responding-to-pr-review-on-a-stack.md)): stage 2 stacks on stage 1 and the stack merges together; neither stage is a shippable milestone on its own.
Each behind its own decision: subagent mode inheritance via a forwarded `AgentOptions.mode` (the option field itself ships), per-tool `ask` policies inside mode definitions (an OpenCode-style "bash asks in plan mode"), preset modes beyond `plan` (read-only, accept-edits), sandbox-backed bash confinement in plan mode, and the idle-record primitive if pending-intent loss proves real.
There is no unmerged prerequisite: the review channel is the user-interaction seam ([ask-user](../../implemented/feature/2026-06-25-ask-user-question.md), on master), so this stack bases on master. The wider in-flight neighborhood is convergent, not conflicting: the sandbox branch's per-session switching pins the same fold-from-log + boundary-application idiom this design follows, its ACP feature-matrix stance on session modes is the single coordination point (resolved in the [FAQ](#faq)), and its scripted-answer harness op is the shape our`elicitationAnswers` mirrors.
### Stage 1 — the mode core
The `dsh-mode` package: `mode/set` + `foldMode`, the assemble filter and mode section, the pre-execute gate, validated Config with the `plan` definition, `ctx.modes` with pending-intent flush, the `AgentOptions.mode` merge, and the stdio toggle. Coverage named at plan time: unit tier for the fold, the filter, the gate matrix, the flush mechanics, and the coalesced boundary notice; a snapshot scenario pinning `mode/set` plus the consequent `request/header-delta` in `session.jsonl`; all existing goldens byte-identical (default mode is invisible). Docs tail in the same stage: package + group READMEs, the [packages map](../../../../packages/README.md) row, regenerated persistence/config catalogs, and the cookbook's plan-mode row upgraded from sketch to package pointer.
### Stage 2 — the exit loop and the protocol surface
`exit_plan_mode` (plan-carrying, review over the user-interaction seam, render intent as specified), the ACP session-mode mapping (`session/set_mode`, `current_mode_update`, advertised available modes), and the snapshot-harness extensions (the `setMode` op, scripted `elicitationAnswers`). Coverage: unit tier for the review's outcome paths (approve, keep-planning with feedback, aborted, no provider); recorded snapshot scenarios driving approve and reject through the scripted elicitation answers; the ACP mode round-trip in the bridge's protocol tests.
Deferred beyond this landing, each behind its own decision: subagent mode inheritance via a forwarded `AgentOptions.mode` (the option field itself ships in stage 1), per-tool `ask` policies inside mode definitions (an OpenCode-style "bash asks in plan mode"), preset modes beyond `plan` (read-only, accept-edits), sandbox-backed bash confinement in plan mode, and the idle-record primitive if pending-intent loss proves real.
The two recorded snapshot scenarios (the full plan-mode arc driving approve and keep-planning through scripted `elicitationAnswers`) await a with-key recording session; the deny and review texts are pinned at the unit tier meanwhile, and the keyless `modes-advertise` scenario pins the wire surface.
## FAQ
Behavioral clarifications of the chosen design; rejected designs live in [Alternatives considered](#alternatives-considered), accepted costs in [Risks](#risks).
Behavioral clarifications of the chosen design; rejected designs live in [Alternatives considered](#alternatives-considered), accepted costs in [Consequences](#consequences).
**When does a user's mode flip take effect?** At the next turn boundary: `set()` records a pending intent, the service flushes it as the first append after the next `turn/start`, and the loop assembles the prompt after the turn opens and before each step — so step 1 already folds it. A mid-turn flip lands at the next boundary and takes effect on the following step. This is the "applies to subsequent requests" semantics every product in [Prior art](#prior-art) ships.
@@ -155,7 +145,7 @@ Behavioral clarifications of the chosen design; rejected designs live in [Altern
**What if a deployment composes no user-interaction provider?** Plan mode stays safe but manual: `ctx.userInteraction.ask()` throws `NO_PROVIDER` (and an absent seam never resolves at all), the tool returns the corrective `isError`, and the exit degrades to the user toggling modes — never to an unreviewed exit. The mode section tells the model to present its plan through `exit_plan_mode` — and to ask the user in prose if that fails — so it never thrashes against the gate.
**Do subagents inherit the parent's mode?** A fork child inherits for free — the parent's `mode/set` is inside the seeded prefix. A spawn child starts in the default mode unless its creator seeds `AgentOptions.mode`; automatic forwarding by subagent providers is deferred ([Roadmap](#roadmap)).
**Do subagents inherit the parent's mode?** A fork child inherits for free — the parent's `mode/set` is inside the seeded prefix. A spawn child starts in the default mode unless its creator seeds `AgentOptions.mode`; automatic forwarding by subagent providers is deferred ([Deferred](#deferred)).
**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs and belong to ACP's `session/set_config_option`; the division this proposal pins is picker-to-modes / knobs-to-config-options. The in-flight sandbox branch already ships both knobs as config-option selects and its feature-matrix stance records session modes as deliberately unmodeled — the one overlap between the two stacks; landing the picker supersedes that stance, and whichever side lands second amends the matrix rows. A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered).
@@ -191,16 +181,16 @@ The ecosystems that leave modes to convention show the failure shapes to avoid.
**Exit by prose or steering instead of a tool.** No artifact and no approval moment — the tool's argument IS the reviewable plan, and its review question is what gives the human a structured yes/no attached to the exact transition.
## Acceptance criteria
## Consequences
- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by the matching `request/header-delta` on the next step with the dev invariant green throughout.
What holds now, pinned by the unit, protocol, and snapshot tiers:
- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by the matching `request/header` event (delta or fallback snapshot) on the next step with the dev invariant green throughout.
- A user-driven flip narrates exactly once at the next boundary and a net-zero flip sequence narrates nothing; a tool-driven exit narrates only through its tool result.
- In the default mode the plugin is invisible: assemblies are byte-identical with and without `dsh-mode` loaded, and every pre-existing snapshot golden is unchanged.
- In plan mode the filtered schemas and mode section reach both the wire request and the logged header; a call to a registered-but-filtered mutating tool is denied at `tools/pre-execute` with the mode-naming reason.
- Mode definitions (allowlist, section text) are changeable from `cordis.yml` with no code edit; an unknown mode name fails validation loudly at `set()` time.
- `exit_plan_mode`'s approve path flips the mode and restores the full toolset on the next step; the keep-planning path returns the corrective `isError` carrying the user's feedback and stays in plan mode; both are pinned by recorded snapshot scenarios through scripted elicitation answers; the ACP `session/set_mode` round-trip updates `current_mode_update`, and the exit review prompts in the terminal through the stdio user-interaction provider.
- The docs tail shipped with the landing: READMEs, regenerated catalogs (persistence log, config, cordis services), and the cookbook row.
- `exit_plan_mode`'s approve path flips the mode and restores the full toolset on the next step; the keep-planning path returns the corrective `isError` carrying the user's feedback and stays in plan mode; the ACP `session/set_mode` round-trip updates `current_mode_update`, and the exit review prompts through each surface's user-interaction provider.
- The docs tail shipped with the landing: READMEs, regenerated catalogs (persistence log, config, cordis services, tools), the packages map and architecture rows, and the cookbook row.
## Risks
A pending user flip set while idle is lost if the process dies before the next turn — accepted (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). Every mode transition is a logged header change and therefore a prefix-cache reset at the provider — inherent, visible in per-step usage, and an argument against mode-flapping UIs, not against the design. Sibling-listener order is not deterministic, so a foreign assemble listener wrapping OUTSIDE the mode listener could re-widen filtered schemas — the filter runs on the assembly `next()` returns (so everything inside its wrap is covered), and the hard gate keeps anything re-widened non-executable; the residual cost is cosmetic (the model sees a tool it cannot use), accepted rather than mechanized. Plan mode's shipped allowlist excludes `bash` and `subagent`, which costs real exploration power (no `git log`, no read-only delegate) until the sandbox family and mode inheritance land — a deployment that accepts the risk can widen its own config today. Two in-flight stacks touch the ACP mode surface (this one and the sandbox branch's config options, whose feature-matrix stance records session modes as deliberately unmodeled): the picker-to-modes / knobs-to-config-options division pinned in the [FAQ](#faq) is the proposed contract, landing order decides who amends, and the second lander owes the matrix rows. The ACP spec's draft v2 direction reportedly slates session modes for removal in favor of config options; if that lands, the picker migrates to a config-option select mechanically — the mode state and both enforcement layers are wire-agnostic — accepted. Branch-heavy policy code under the per-file 100% coverage gate is real work, accepted as the ACP bridge did.
The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). Every mode transition is a logged header change and therefore a prefix-cache reset at the provider — inherent, visible in per-step usage, and an argument against mode-flapping UIs, not against the design. Sibling-listener order is not deterministic, so a foreign assemble listener wrapping OUTSIDE the mode listener could re-widen filtered schemas — the filter runs on the assembly `next()` returns, and the hard gate keeps anything re-widened non-executable; the residual cost is cosmetic (the model sees a tool it cannot use), accepted rather than mechanized. Plan mode's shipped allowlist excludes `bash` and `subagent`, which costs real exploration power until the sandbox family and mode inheritance land — a deployment that accepts the risk can widen its own config today. Two in-flight stacks touch the ACP mode surface (this one and the sandbox branch's config options, whose feature-matrix stance records session modes as deliberately unmodeled): the picker-to-modes / knobs-to-config-options division pinned in the [FAQ](#faq) is the contract, and the sandbox branch owes its matrix rows an amendment on merge-down. The ACP spec's draft v2 direction reportedly slates session modes for removal in favor of config options; if that lands, the picker migrates to a config-option select mechanically — the mode state and both enforcement layers are wire-agnostic — accepted.
The coding agent as an ACP server with **session modes** composed — the live composition of [the plan-mode RFC](../../docs/rfc/proposed/feature/2026-07-07-plan-mode.md).
The coding agent as an ACP server with **session modes** composed — the live composition of [the plan-mode RFC](../../docs/rfc/implemented/feature/2026-07-07-plan-mode.md).
@@ -6,4 +6,4 @@ Session modes: named, logged, per-agent policy states, with **plan mode** as the
|---|---|---|
| `mode/` | `mode/set` vocabulary + fold, the `ctx.modes` service (list/get/set with the turn-boundary flush), the soft layer (assemble filter + `mode:policy` section), the hard layer (`tools/pre-execute` deny-by-default gate), and the model-facing `exit_plan_mode` review tool | `ctx.modes` |
The mode in force is a pure function of the session log (`SessionEventMap['mode/set']`, last one wins), so resume and fork restore it with no extra machinery; the default mode is the absence of policy, keeping the plugin invisible until a mode is set. UIs read flips off `session/event`: the [stdio app](../ui/stdio-agent) exposes `/mode`, the [ACP bridge](../ui/acp) maps the vocabulary to the session-mode picker. RFC: [plan mode](../../docs/rfc/proposed/feature/2026-07-07-plan-mode.md).
The mode in force is a pure function of the session log (`SessionEventMap['mode/set']`, last one wins), so resume and fork restore it with no extra machinery; the default mode is the absence of policy, keeping the plugin invisible until a mode is set. UIs read flips off `session/event`: the [stdio app](../ui/stdio-agent) exposes `/mode`, the [ACP bridge](../ui/acp) maps the vocabulary to the session-mode picker. RFC: [plan mode](../../docs/rfc/implemented/feature/2026-07-07-plan-mode.md).
@@ -39,4 +39,4 @@ The model-facing exit tool. Its single required argument is the plan text — a
Definitions are validated at load (`resolveConfig`): the built-in `plan` (read-only allowlist, `bash`/`subagent` excluded) merges unless overridden, `default` is rejected as a key, and allowlists may name not-yet-registered tools (registration is dynamic). An unknown name fails loudly at `set()` time.
@@ -111,7 +111,7 @@ Tool-call presentation is **owned by each tool** (`presentCall` / `presentResult
## 6. Session modes / config options / models
Session modes ✅ (the [plan-mode RFC](../../../docs/rfc/proposed/feature/2026-07-07-plan-mode.md)): the picker maps 1:1 onto `dsh-mode`'s vocabulary — `ctx.modes.list()` fills `availableModes`, `session/set_mode` calls `set()` (pending intent, flushed at the turn boundary), and `current_mode_update` tracks both the optimistic echo and every logged flip. The division is picker-to-modes / knobs-to-config-options: individual environment knobs (sandbox mode, approval policy, the model) are NOT modes and belong to `session/set_config_option` — still unbuilt here, as is runtime model selection (the harness fixes the model per-bridge via `AcpConfig.model`). The ACP draft v2 direction reportedly slates session modes for removal in favor of config options; if that lands, the picker migrates mechanically (the mode state and both enforcement layers are wire-agnostic).
Session modes ✅ (the [plan-mode RFC](../../../docs/rfc/implemented/feature/2026-07-07-plan-mode.md)): the picker maps 1:1 onto `dsh-mode`'s vocabulary — `ctx.modes.list()` fills `availableModes`, `session/set_mode` calls `set()` (pending intent, flushed at the turn boundary), and `current_mode_update` tracks both the optimistic echo and every logged flip. The division is picker-to-modes / knobs-to-config-options: individual environment knobs (sandbox mode, approval policy, the model) are NOT modes and belong to `session/set_config_option` — still unbuilt here, as is runtime model selection (the harness fixes the model per-bridge via `AcpConfig.model`). The ACP draft v2 direction reportedly slates session modes for removal in favor of config options; if that lands, the picker migrates mechanically (the mode state and both enforcement layers are wire-agnostic).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.