1
0
Эх сурвалжийг харах

docs: synthesize architecture rewrite

Tianyi Cui 3 сар өмнө
parent
commit
fdd65269f9

+ 87 - 63
docs/architecture.md

@@ -1,24 +1,22 @@
 # DeepSeek Harness Architecture
 
-DeepSeek Harness SDK is the plugin runtime for building agent harnesses. The governing idea is small and strict: the harness core owns the vocabulary and the turn driver, and every product capability is a Cordis plugin attached through a typed service or event seam. Model adapters, tools, persistence, filesystem access, hooks, UI bridges, compaction, and subagents all enter the system the same way.
+The **DeepSeek Harness SDK** is an agent-runtime SDK built microkernel-style on the vendored Cordis framework. The governing principle is simple: **everything is a plugin**. The shipped loop plugin drives the default agent lifecycle, but it is still replaceable; most behavior attaches through typed service and event seams that a replacement loop would honor.
 
-Read this page as the system map before changing `packages/`. It describes behavior: what services exist, how a turn moves through the loop, where plugins extend it, and which invariants keep replay and hot reload sane. Literal type shapes live in [core-data-structures/](core-data-structures/core.md), exact event and service signatures live in the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs, visual relationship maps live in the [documentation graph index](graph-atlas.md), and package-level contracts live in the package READMEs from the [package map](../packages/README.md).
+Read this page as the system map before changing `packages/`. It covers behavior: services, loop lifecycle, extension seams, and invariants. Type shapes live in [core-data-structures/](core-data-structures/core.md); exact signatures in the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs; diagrams in the [documentation graph index](graph-atlas.md); package contracts in the [package map](../packages/README.md); rationale in the [RFCs](rfc/README.md).
 
-## The Mental Model
+## Mental Model
 
-A running harness is one Cordis context. Packages contribute three kinds of things to it:
+A running harness is one Cordis context. Packages contribute three things to it:
 
 - **Services** on `ctx.<key>`: stable call surfaces such as `ctx.llm`, `ctx.tools`, or `ctx.sessions`.
-- **Events**: interception and notification seams such as `agent/request`, `tools/pre-execute`, or `session/event`.
-- **Registrations**: prompt sections, tool schemas, adapters, providers, and listeners, all installed through `ctx.effect()`, `ctx.on()`, or `ctx.waterfall()` so disposal unwinds them.
+- **Events**: typed interception and notification seams such as `agent/request`, `tools/pre-execute`, or `session/event`.
+- **Registrations**: prompt sections, tool schemas, adapters, providers, and listeners, all installed through disposable effects so teardown and hot reload unwind them.
 
-The default agent loop is intentionally ordinary: drain queued work, assemble a request, stream a model answer, run tools, decide whether to continue, flush durable state. The important part is where the loop pauses. Every pause is a seam a plugin can program against without reaching into loop internals.
-
-That rule is the design pressure behind the repo layout. Interface packages own vocabulary and `ctx` keys; implementation packages register concrete backends; consumer packages expose model-facing tools or app-facing bridges. A product feature should usually be a plugin on an existing seam, not a patch to `dsh-agent-loop`.
+The default loop is intentionally ordinary: drain queued work, assemble a request, stream a model answer, run tools, decide whether to continue, flush durable state. The important part is where it pauses: each pause is a seam a plugin can program against.
 
 ## Service Map
 
-The product spine in [`packages/core/`](../packages/core/README.md) is the minimum language of an agent run:
+The default agent spine is assembled from these packages under [`packages/core/`](../packages/core/README.md):
 
 | ctx key | Package | Role |
 |---|---|---|
@@ -26,9 +24,11 @@ The product spine in [`packages/core/`](../packages/core/README.md) is the minim
 | `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections plus tool schemas |
 | `ctx.tools` | `dsh-tools` | tool registry and execution pipeline |
 | `ctx.agents` | `dsh-agent` | live agent registry, public `Agent` handle, `agent/*` vocabulary |
-| `ctx.agentLoop` | `dsh-agent-loop` | the concrete `ReactLoopAgent` driver |
+| `ctx.agentLoop` | `dsh-agent-loop` | the shipped `ReactLoopAgent` driver |
+
+Tool schemas ride in prompt assembly, so "what the model is told it can do" stays coherent ([assembly RFC](rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md)). Tool execution runs through `tools/pre-execute` → dispatch → `tools/post-execute`, the gate pair for sandbox, permission, hook, and plan-mode plugins ([pipeline graph](tool-execution-pipeline.md)).
 
-The swappable seams sit around that spine:
+The swappable capability seams sit around that spine:
 
 | ctx key | Package family | Role |
 |---|---|---|
@@ -40,82 +40,106 @@ The swappable seams sit around that spine:
 | `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers |
 | `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable storage for session logs |
 
-`dsh-agent-core` is the sanctioned composition exception to the dependency rule: it depends on the concrete loop because its job is to assemble the default providerless spine. Extension plugins depend on interfaces and event vocabulary, never on `dsh-agent-loop`; swapping the loop means shipping a different bundle, not rewiring every extension.
-
-## Capability Seams
+Extension plugins depend on interfaces and event vocabulary, never on `dsh-agent-loop`; swapping the loop means shipping a different bundle. The sanctioned exception is `dsh-agent-core`, whose job is to compose the default spine.
 
-The default seam shape is **interface / implementation / consumer**. The interface package owns the `ctx` key, abstract service, event vocabulary, and shared types. An implementation package registers one concrete backend. A consumer package, often a `tool-*` package, depends only on the interface and registers model-facing behavior through `ctx.tools` or prompt assembly. The bash family is the reference shape: `dsh-bash`, `dsh-bash-local`, `dsh-tool-bash`.
+## Cordis In Five Ideas
 
-Several seams intentionally bend that template. The LLM seam keeps interface and consumer vocabulary together because adapters are the only implementations. The filesystem family adds `dsh-fs-policy` as an event-gate plugin: `dsh-tool-fs` dispatches `fs/write-intent`, `fs/edit-intent`, and `fs/observed`, while the policy listens without becoming a method service the tool must inject. The web seam is one service with search and fetch provider registries, so provider swaps do not rename model tools. The subagent seam is a named provider registry because multiple delegation backends can coexist in one context.
+Cordis is vendored source the harness owns ([manifest + sync](../vendor/README.md)). A plugin author needs five ideas: plugins are modules with optional `inject` and `apply(ctx)`, or `Service` subclasses; services own `ctx` keys; `inject` waits for required services; events are typed by declaration merging and dispatch as emit, waterfall, parallel, or serial; registrations are disposable effects.
 
 ## Cordis Waterfall Semantics
 
-`ctx.waterfall` is around-middleware, not a reducer. A listener receives `(...args, next)` and chooses one of three behaviors:
+`ctx.waterfall` is around-middleware, not a reducer. A listener receives `(...args, next)`: call `next()` to delegate, optionally wrapping the result; return without `next()` to short-circuit; use `prepend: true` only when it must run first. Values propagate through `next()`'s return value. Cooperative listeners mutate a shared object and then delegate; returning a replacement is a takeover because earlier mutations on the old object disappear downstream. For single-slot decision events such as `fs/write-intent`, returning without `next()` is the point: the first decider owns the decision.
 
-- call `next()` to delegate to later listeners and the core behavior, optionally wrapping the result;
-- return without calling `next()` to short-circuit with its own result;
-- register with `prepend: true` when it must run before existing listeners.
+## Event Taxonomy
 
-Values propagate through `next()`'s return value. Cooperative listeners mutate a shared object and then delegate; replacing an object is a takeover, because earlier mutations on the old object will not be seen downstream. For single-slot decision events such as `fs/write-intent`, returning without `next()` is the point: the first decider owns the decision.
+Each service declares its own events; `agent/*` lives in `dsh-agent`, so extensions use the live agent vocabulary without depending on the concrete loop. Capability events belong to the seam that owns their vocabulary: `tools/*`, `llm/*`, `system-prompt/*`, `fs/*`, `subagent/*`, and `session/flush`. The generated [events catalog](cordis-catalog/events.md) is exhaustive; [event-producer-consumer.md](event-producer-consumer.md) shows topology.
 
-## Sessions And Messages
+Domain rule: `session/*` is durable, replayable fact; `agent/*` is live runtime surface ([event-domain RFC](rfc/implemented/architecture/2026-06-30-event-domain-semantics.md)). Reloadable UI state belongs on the session log; hooks, status observers, request mutation, prompt gating, step-result validation, and continuation policy belong on the live agent surface.
 
-A `Session` is an append-only log of typed `SessionEvent`s. The log is the source of truth for replay, UI rendering, persistence, and derived model history. `deriveMessages()` projects surface events into the `Message[]` sent to the model; raw `assistant/chunk` entries stay in the log for replay and transcript fidelity but do not become prompt history. Persistence backends subscribe to `session/event`, buffer snapshots of appended events, and drain them at the awaited `session/flush` checkpoint.
+## Loop Lifecycle (Session / Turn / Step)
 
-Messages are arrays of typed content blocks from `dsh-llm`: `text`, `reasoning`, `tool-call`, and `tool-result`. The block union, message sources, finish reasons, turn triggers, turn-end reasons, and session event variants use the merge-extensible-map pattern documented in [core-data-structures](core-data-structures/core.md). A plugin can extend a map, but every shipping path that observes the new variant must be taught what it means.
+A **session** is one agent's append-only event log. A **turn** drains one queued batch and runs until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the contract below ([sequence companion](agent-lifecycle.md)), every `'quoted'` line appends a durable session event and every `waterfall`/`serial` line is an extension seam.
 
-## Loop Lifecycle
+### Turn Flow
 
-The loop uses three nested units:
+```text
+create agent -> emit agent/session-start(source)    once per live agent
+forever:
+  wait for queued messages (idle)
+  emit agent/status(running)
+  TURN:
+    'turn/start'
+    each queued msg: waterfall agent/prompt-submit  allow, rewrite, attach context, or block
+      allow -> session('user/message'...); inject additionalContext
+    every prompt blocked -> 'turn/end'(rejected)    zero-step turn, model never called
+    STEP loop:
+      drain steering
+      assembly = ctx.systemPrompt.assemble()        waterfall system-prompt/assemble
+      await ctx.serial('agent/pre-step')            surface mutation before history derivation
+      session('step/start')
+      req = {model, system, tools, messages: session.deriveMessages(), signal}
+      req = waterfall agent/request                 hooks, model switch, tool filtering
+      stream ctx.llm.stream(req)                    waterfall llm/stream
+        session('assistant/chunk')
+      if assembler.finish is error/aborted: throw
+      msg = waterfall agent/step-result             before the log append
+      session('assistant/message' {content, usage?})
+      each tool-call (sequential, abort-checked between calls):
+        session('tool/call'); ctx.tools.execute()
+          tools/pre-execute -> dispatch -> tools/post-execute
+          tools may append their own session events, e.g. todo/write
+        session('tool/result')
+      append buffered post-execute context -> session('context/message')*
+      drain steering -> session('steering/message')
+      session('step/end')
+      cont = waterfall agent/turn-continuation      continue iff tool calls or steering by default
+      continue reasons become next-step steering
+      if action == stop: break
+  session('turn/end')
+  await ctx.parallel('session/flush', session)
+  leftover steering re-enqueued as queued messages
+  emit agent/status(idle) unless more queued
+```
 
-- **Session**: the full append-only event log for one agent.
-- **Turn**: one drained batch of queued work, running until the model stops asking for tools and no plugin requests continuation.
-- **Step**: one model request plus the tool executions caused by that response.
+Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; leftover steering after a turn is re-queued.
 
-One turn follows this shape:
+### Failure Boundaries
 
-```text
-agent/session-start                    once per live agent
-turn/start                             durable boundary
-  agent/prompt-submit                  allow, rewrite, attach context, or block each queued prompt
-  system-prompt/assemble               sections + tool schemas
-  agent/pre-step                       surface mutation before history derivation, e.g. compaction
-  step/start                           durable boundary
-    agent/request                      mutate the GenerateOptions before the model call
-    llm/stream                         stream raw chunks from the selected adapter
-    assistant/chunk*                   replay/UI facts
-    agent/step-result                  inspect or rewrite the assembled assistant message
-    assistant/message                  the message used for tool dispatch and future history
-    tool/call -> ctx.tools.execute -> tool/result
-      tools/pre-execute                allow, deny, or ask before dispatch
-      tools/post-execute               accept, block, replace output, or attach context
-    context/message*                   buffered post-tool context, after all tool results
-    steering/message*                  mid-turn steering for the next step
-  step/end                             durable boundary
-  agent/turn-continuation              continue or stop
-turn/end                               durable boundary
-session/flush                          awaited durability checkpoint
-```
+The turn is the containment boundary. A throwing listener, adapter error finish, or failed step ends the current turn with an error reason and reports live diagnostics through `agent/error`; it does not kill the driver loop. `cancel()` clears queued and steering work, aborts the active model/tool boundary when possible, and records the appropriate turn end. Disposal stops the loop, awaits quiescence, unregisters the agent, and lets service disposers drain.
 
-Tool calls are sequential, and the loop checks cancellation between calls. Post-tool context is appended after all tool results so the tool-call/result adjacency remains stable. Steering injected while a turn is running is drained between steps; leftover steering after a turn is re-queued so it is never stranded.
+Every session event is turn-enclosed. Reloading a crashed session preserves the interrupted tail and closes it with a synthetic `interrupted` turn end. A failure after `turn/end`, such as a rejecting `session/flush`, reports through `agent/error` only because no safe in-turn position remains. A turn ends with one `TurnEndReason` (`completed`, `aborted`, `error`, `disposed`, `max-tokens`, `rejected`, or `interrupted`); per-variant semantics are in [session.md § TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap).
 
-## Event Domains
+## Agents And Subagents
 
-`session/*` events are durable, replayable facts. Anything a UI can reconstruct after reload, including transcript surface, todo state, hook provenance, compaction records, and crash recovery markers, belongs on the session log or a merge-extensible session event.
+`Agent` is the handle every plugin programs against: `send()` queues work, `steer()` injects mid-turn content, `inject()` appends context and opens a one-shot `injection` turn when idle, `cancel()` is the single public stop primitive, and `whenIdle()` observes quiescence. The factory returns `AgentHandle { agent, dispose() }`; lifecycle owners tear down with `await dispose()`. Full semantics: [core.md](core-data-structures/core.md), [lifecycle RFC](rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md).
 
-`agent/*` events are the live runtime surface. They carry an `Agent` object and power hooks, status observers, request mutation, prompt gating, step-result validation, and continuation policy. The declarations live in `dsh-agent`, not in `dsh-agent-loop`, so plugins can depend on the public agent vocabulary without depending on the concrete loop.
+**Subagents** are a seam, not an `Agent` method: `ctx.subagents` is a named-provider registry (`spawn` starts fresh, `fork` seeds from the parent's completed-turn prefix, ACP drives an out-of-process child); children are ordinary agents ([subagent.md](core-data-structures/subagent.md), [subagent RFC](rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)).
+
+## The Session Log Is The Truth
+
+A `Session` is the single source of truth. `deriveMessages()` projects surface events into the `Message[]` sent to the model; raw `assistant/chunk` events stay in the log for replay/UI fidelity and are skipped. Every other consumer is a derived view too: replay/fork seeds from events, trace/telemetry listens to `session/event`, and resume goes through `ctx.agents.resume({ resumeSessionId })` ([event-sourcing RFC](rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md)).
+
+Durability is a plugin concern: `session/event` is synchronous, persistence backends buffer write-behind, and the loop awaits `session/flush` at every turn end. The `SessionPersistence` seam stores `SessionEvent` directly, with metadata in `SessionHeader`; JSONL and SQLite share one contract suite ([persistence RFC](rfc/implemented/architecture/2026-06-14-session-persistence.md), [write-coordinator RFC](rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)).
+
+## Content Blocks And Streaming (dsh-llm)
+
+Messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`). The union derives from the merge-extensible `ContentBlockMap`; the same pattern types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. The core set is limited to blocks every shipping path honors; new block types land in one coordinated change across adapters, UI bridges, and compaction pricing ([drop-image RFC](rfc/implemented/simplification/2026-07-04-drop-image-content-block.md)).
+
+Streaming is a raw chunk protocol (`block-start` through `finish`) with `BlockAssembler` as the shared chunk-to-block assembler. The loop logs raw chunks for replay while assembling them for dispatch. `LlmAdapter` is the provider seam: subclass, implement `stream()`, register with `ctx.llm.registerAdapter(models, adapter)`; `dsh-llm-deepseek` and `dsh-llm-pi-ai` are deliberate design twins ([twin RFC](rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md)). StreamChunk conventions live in [llm-streaming.md](core-data-structures/llm-streaming.md).
+
+## Capability Seams
 
-Capability events belong to the seam that owns their vocabulary: `tools/*` for tool execution, `llm/*` for model streaming, `system-prompt/*` for assembly, `fs/*` for filesystem policy, `subagent/*` for delegation runs, and `session/flush` for durability. The generated [events catalog](cordis-catalog/events.md) is the exhaustive reference and is freshness-gated.
+A swappable capability splits into **interface / implementation / consumer**: the interface owns the `ctx` key and vocabulary; an implementation registers a backend; a consumer exposes model-facing behavior through `ctx.tools` or prompt assembly. The bash trio is the reference shape, and the mechanism is plain Cordis services plus `inject` ([capability-seams RFC](rfc/implemented/architecture/2026-06-13-capability-seams.md), [seam graph](capability-seams.md)).
 
-## Failure Boundaries
+Some seams bend the template deliberately. LLM keeps interface and consumer vocabulary together because adapters are the implementations. Filesystem adds policy as an event gate: `dsh-tool-fs` dispatches `fs/write-intent`, `fs/edit-intent`, and `fs/observed`, while `dsh-fs-policy` listens without becoming a method service ([event-gate RFC](rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md)). Web is one service with search and fetch provider registries, so provider swaps do not rename model tools ([web-seam RFC](rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). Subagents use a named provider registry because multiple delegation backends can coexist.
 
-The turn is the loop's containment boundary. A throwing listener, adapter error finish, or failed step ends the current turn with an error reason and reports live diagnostics through `agent/error`; it does not kill the driver loop. `cancel()` clears queued and steering work, aborts the active model/tool boundary when possible, and records the appropriate turn end. Disposal stops the loop, awaits quiescence, unregisters the agent, and lets service disposers drain their work.
+## Composition
 
-Every session event is turn-enclosed. A backend that reloads a crashed session preserves the interrupted tail and closes it with a synthetic `interrupted` turn end rather than truncating real work. A failure after `turn/end`, such as a rejecting `session/flush`, is reported through `agent/error` only because there is no safe in-turn position left for a durable session event.
+`dsh-agent-core` is the composition bundle: one plugin loading the providerless spine as code ([README](../packages/core/agent-core/README.md)). App packages compose it with a front door and own the boot `bin`: `dsh-stdio-agent` for the terminal REPL, and `dsh-acp-agent` for ACP over JSON-RPC stdio with no stdout logger ([ui/](../packages/ui/README.md), [app-extraction RFC](rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
 
 ## Extending The Harness
 
-Start from the extension point, not the loop:
+New behavior should attach to a documented seam; changing `dsh-agent-loop` itself requires updating this map.
 
 | Goal | Mechanism |
 |---|---|
@@ -126,4 +150,4 @@ Start from the extension point, not the loop:
 | Add UI or editor integration | drive `ctx.agents` and render from `session/event` |
 | Add durable session state | add a `SessionEventMap` member and render/replay from the log |
 
-The [extension cookbook](cookbook/extension-cookbook.md) maps common features to seams, and the step-by-step guides cover [packages](cookbook/adding-a-package.md), [tools](cookbook/adding-a-tool.md), [LLM adapters](cookbook/adding-an-llm-adapter.md), and [vendored packages](cookbook/adding-a-vendored-package.md). When a change seems to require editing `dsh-agent-loop`, first name the missing seam; if the loop really changes, update this map in the same PR.
+The [extension cookbook](cookbook/extension-cookbook.md) carries plugin skeletons and the feature-to-seam map; step-by-step guides cover [packages](cookbook/adding-a-package.md), [tools](cookbook/adding-a-tool.md), [LLM adapters](cookbook/adding-an-llm-adapter.md), and [vendored packages](cookbook/adding-a-vendored-package.md).

+ 1 - 1
docs/rfc/implemented/process/2026-06-20-rfc-classification.md

@@ -30,7 +30,7 @@ The `architecture` / `process` line: **architecture** is about the source we shi
 Both are `doc-sync` members, in the `verify-md-wrap` style (tsx ESM, verify-don't-generate, exit non-zero on the first violation):
 
 - **`scripts/verify-rfc-classification.ts`** — the closed set and index freshness. It asserts every file under a lifecycle folder lives in a class folder from the canonical set (a loose `.md` at a lifecycle root, or an unknown class folder, fails), and that the README's marker-delimited index regions byte-match a fresh render from the tree (see [generate the RFC index tables](2026-07-04-generate-rfc-index-tables.md)). The canonical class set lives as a `const` in `scripts/rfc-index.ts` — the machine source of truth shared with the generator — and [the index](../../README.md) documents it in prose; the README's class *descriptions* stay hand-written, its tables are generated.
-- **`scripts/verify-doc-refs.ts`** — source comments that cite docs. RFC paths are referenced not only from Markdown but from TypeScript doc comments (root-relative prose like `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`). `verify-md-links` never saw those, so the reorg could have silently orphaned them. This gate scans repo-authored `.ts` under `packages/**` and `examples/**` (excluding built `lib/` and `vendor/`) for `docs/….md` tokens, resolves each root-relative, and asserts it exists. It requires the `.md` extension so extensionless prose (`docs/postmortem/0001`, `docs/architecture.md § plugin checklist`) is left alone.
+- **`scripts/verify-doc-refs.ts`** — source comments that cite docs. RFC paths are referenced not only from Markdown but from TypeScript doc comments (root-relative prose like `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`). `verify-md-links` never saw those, so the reorg could have silently orphaned them. This gate scans repo-authored `.ts` under `packages/**` and `examples/**` (excluding built `lib/` and `vendor/`) for `docs/….md` tokens, resolves each root-relative, and asserts it exists. It requires the `.md` extension so extensionless prose (`docs/postmortem/0001`, `docs/architecture.md § Extending The Harness`) is left alone.
 
 ### Rejected alternatives
 

+ 1 - 1
packages/bash/bash-local/README.md

@@ -27,4 +27,4 @@ Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi;
 
 ## Sandboxing
 
-`TODO(permissions/sandbox)`: execution policy does NOT belong in this package. Use the `tools/pre-execute` deny/ask gate or implement a sandboxing `BashExecutor` — see docs/architecture.md § plugin checklist. Reference points: Claude Code wraps commands in sandbox-exec/bubblewrap; Codex applies seatbelt/landlock plus an execpolicy prefix-rule engine.
+`TODO(permissions/sandbox)`: execution policy does NOT belong in this package. Use the `tools/pre-execute` deny/ask gate or implement a sandboxing `BashExecutor` — see docs/architecture.md § Extending The Harness. Reference points: Claude Code wraps commands in sandbox-exec/bubblewrap; Codex applies seatbelt/landlock plus an execpolicy prefix-rule engine.

+ 3 - 2
packages/bash/bash-local/src/index.ts

@@ -5,8 +5,9 @@
  * survey notes), tracks background tasks, and kills everything on dispose.
  *
  * TODO(permissions/sandbox): execution policy does NOT belong here — use
- * the `tools/pre-execute` deny/ask gate (see docs/architecture.md § plugin
- * checklist) or implement a sandboxing `BashExecutor`. Reference points:
+ * the `tools/pre-execute` deny/ask gate (see docs/architecture.md
+ * § Extending The Harness) or implement a sandboxing `BashExecutor`.
+ * Reference points:
  * Claude Code wraps commands in sandbox-exec/bubblewrap; Codex applies
  * seatbelt/landlock plus an execpolicy prefix-rule engine.
  *

+ 1 - 1
packages/bash/tool-bash/src/index.ts

@@ -33,7 +33,7 @@
  * TODO(permissions): commands run with the executor's full authority. The
  * permission/sandbox seam is the `tools/pre-execute` waterfall (deny/ask) plus
  * sandboxing `BashExecutor` implementations — see docs/architecture.md
- * § plugin checklist.
+ * § Extending The Harness.
  *
  * @module @deepseek-ai/dsh-tool-bash
  */

+ 1 - 1
scripts/doc-budgets.manifest.json

@@ -1,7 +1,7 @@
 {
   "AGENTS.md": 1575,
   "docs/AGENTS.md": 1315,
-  "docs/architecture.md": 1650,
+  "docs/architecture.md": 1840,
   "docs/defensive-patterns.md": 550,
   "docs/testing.md": 800,
   "examples/AGENTS.md": 610,

+ 3 - 3
scripts/verify-doc-refs.ts

@@ -2,7 +2,7 @@
  * Doc-sync gate: verify that doc references written in TypeScript COMMENTS
  * resolve to a file that exists. Source comments cite docs by root-relative
  * prose path — `see docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`,
- * `docs/architecture.md § plugin checklist`. `verify-md-links` parses Markdown
+ * `docs/architecture.md § Extending The Harness`. `verify-md-links` parses Markdown
  * link AST and never sees these, so a doc rename or move could silently orphan
  * a `.ts` comment that points at it. The RFC classification reorg
  * ([the classification RFC](../docs/rfc/implemented/process/2026-06-20-rfc-classification.md))
@@ -12,7 +12,7 @@
  * Detection is a token scan, NOT an AST walk: doc refs live in free prose inside
  * comments, not in a structured form. We match `docs/<path>.md` tokens and
  * REQUIRE the `.md` extension, so extensionless prose (`docs/postmortem/0001`,
- * `docs/architecture.md § plugin checklist` — the section suffix is outside the
+ * `docs/architecture.md § Extending The Harness` — the section suffix is outside the
  * token) is left alone rather than misread as a path. Each token is resolved
  * ROOT-RELATIVE (the way the comments are written) and must exist on disk. This
  * is checker, not fixer: it reports and never rewrites.
@@ -43,7 +43,7 @@ const isExcluded = (p: string): boolean =>
  * Match a `docs/…​.md` reference token. The `.md` extension is required so a
  * bare `docs/postmortem/0001` (no extension) does not register as a path. The
  * character class stops at whitespace, backticks, parens, and the section sign,
- * so trailing prose (`… .md § plugin checklist`) is not swallowed into the path.
+ * so trailing prose (`… .md § Extending The Harness`) is not swallowed into the path.
  */
 const DOC_REF = /\bdocs\/[A-Za-z0-9._/-]+\.md/g