# DeepSeek Harness source analysis: when “everything is a plugin” is pushed until there is no kernel

> **Subject**: [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) (product name **dsh**)
> **Baseline**: `v0.1.0-rc.5` @ commit **`47f9438`** (`Merge pull request #2519 from deepseek-harness/feat/npm-public`, 2026-08-13)
> **License**: MIT (`LICENSE`) · third-party disclosures in `THIRD_PARTY_NOTICES.md`
> **Code scale**: **497 489 lines of TypeScript** (2 319 files) + **66 633 lines of TSX** (259 files) + 4 286 lines of Python; **241 workspace packages**
> **Service and tool surface**: **73 `ctx` service keys** · **47 model-visible tools** · **28 `verify-*` gates** · **686 design-decision documents**
> **One-sentence product**: DeepSeek’s official open-source agent harness. The architecture is **“everything is a plugin”** — the model adapter, the tool registry, the session log, **and even the agent loop itself** are plugins, all replaceable from configuration.
> **Audience**: engineers and architects who have read any coding-agent analysis in this series.
> **Narrative**: five-beat **motivation → constraint → rejected alternatives → choice → cost**. Every fact is tagged `file:line` so you can check the source.
> **Local baseline path**: `参考项目/deepseek-harness @ 47f9438`
> **Companion tutorial**: [Build a pluginized agent harness from scratch](../从零构建插件化AgentHarness-DeepSeekHarness系-开发全流程教程.md) (20 steps to a minidsh)

---

## Contents

**Part I · Origin: why DeepSeek built its own harness**
1. [Pain and insight: a model company is missing a skeleton, not a model](#ch1)
2. [Evidence of motive: the original wording in README / AGENTS.md / the paper](#ch2)
3. [Macro stack: the numeric shape of 241 packages and the direction of dependence](#ch3)

**Part II · Constitution: five ideas and three layers of order**
4. [A developer mind map: every design follows from “everything is a plugin”](#ch4)
5. [Cordis’s five ideas: spatially replaceable × temporally unwindable](#ch5)
6. [The capability seam: three roles, or it is not a seam](#ch6)
7. [Three event domains and four dispatch modes: “may mutate” is written into the type](#ch7)

**Part III · Spine: loop, log, tools**
8. [Turn and step: one loop, two boundary levels](#ch8)
9. [Inbox and `agent/turn-stopping`: keep the loop going without changing the loop](#ch9)
10. [“Model-visible ⟺ logged”: turning the constitution into a runtime assertion](#ch10)
11. [The tool pipeline: three waterfalls + monotonic guards + frozen results](#ch11)
12. [System prompt and scope: reassemble every step, isolate per agent](#ch12)

**Part IV · Context economics and task governance**
13. [Four service keys for one concern: compaction / spill / pruner / tokenMeter](#ch13)
14. [goal / plan / todo / schedule / jobs: “no free-form judgment” as plugins](#ch14)
15. [subagent, 11 packages: from a child process to “one turn of someone else’s product”](#ch15)
16. [skill / hooks / mcp: living with other people’s ecosystems](#ch16)

**Part V · Execution world and remote surface**
17. [One execution world: swap two providers, five capabilities move with them](#ch17)
18. [Sandbox: four-platform `confine(argv)` and “isolation strength is queryable”](#ch18)
19. [typert: type-graph-driven RPC, and the ambition of 11 packages](#ch19)
20. [Four fronts, one behavior: CLI / Web / ACP / SDK](#ch20)

**Part VI · Full roster of 241 packages and the collaboration graph**
21. [How to read the roster, and the six functional domains](#ch21)
22. [Domain I · Spine (30 packages) and Domain II · Context economics (11 packages)](#ch22)
23. [Domain III · Execution world (40 packages) and Domain IV · Task governance (39 packages)](#ch23)
24. [Domain V · Assembly and host (51 packages) and Domain VI · Frontends and support (70 packages)](#ch24)
25. [Collaboration graph: in-degree, assembly chains, plugin participation in one turn](#ch25)

**Part VII · Engineering constitution: the strongest set this series has seen**
26. [Per-file 100% coverage: coverage as a deletion tool](#ch26)
27. [28 `verify-*` gates: welding spoken convention into machine gates](#ch27)
28. [686 Agent Notes: institutionalizing the archaeology of design decisions](#ch28)
29. [Dynamic Cordis: the agent mounts its own plugins](#ch29)

**Part VIII · Taste and bounds**
30. [Sixteen decisions, five-beat recap](#ch30)
31. [Cross comparison: harness-shaped vs loop-shaped](#ch31)
32. [Honest bounds](#ch32)
33. [Source tour index and local reproduction](#ch33)

---

<div class="part-band"><span class="band-k">Part I</span>Origin: why DeepSeek built its own harness</div>

<h2 id="ch1">Chapter 1 · Pain and insight: a model company is missing a skeleton, not a model</h2>

### 1.1 Answer “why” first

When a model company open-sources an agent framework, the motive is not the same as an application company’s. An application company wants a product. A model company wants **a carrier that can fully release the model’s capability**, and that carrier has to be reshapeable by outsiders into any form.

DeepSeek Harness’s README is only 55 lines, but the first sentence already states the architectural stance (`README.md:7`):

> It uses an architecture where **everything is a plugin**, and is powered by [Cordis](https://github.com/cordiverse/cordis), whose design is described in *A Programming Paradigm for Spatiotemporal Composability*.

Three points of extremely high information density:

1. **Everything is a plugin** — not “plugins are supported,” but “**only** plugins.”
2. **The substrate is Cordis**, an independent plugin framework with a paper, vendored into the repository.
3. The paper title’s **spatiotemporal composability** — in engineering terms: spatially, services are replaceable by key; temporally, registrations unwind in reverse order.

### 1.2 The concrete pain it is solving

Of the 20-plus agent projects this series has analyzed, the overwhelming majority have this shape: one main loop, plus a pile of features that grew around the main loop. That shape has a shared disease:

> **Every new requirement comes back and patches the main loop.**

Permission confirmation inserts itself before tool execution, context compaction inserts itself into the while, swapping model vendors edits the adapter function, supporting a second frontend copies a loop. Three months later the main loop is a thousand-line mud ball, and **no external contributor can extend it without changing it**.

dsh’s insight: **first build a runtime with no privileged kernel, then make the loop itself a plugin.**

The sentence at `docs/architecture.md:13` is the spine of the entire architecture document:

> **There is no privileged core to patch**: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.

“There is no privileged kernel to patch” — you extend dsh by **mounting a new plugin beside the others**, not by stuffing code into the center.

### 1.3 Three verifiable engineering claims

| Claim | How to verify | Result |
|---|---|---|
| **The loop itself is a plugin** | Is `agent-loop` a package? | Yes. `packages/core/agent-loop`, registered through `ctx.agentLoop` (`src/index.ts`), replaceable from configuration |
| **Configuration is architecture** | Can you print the tree that actually boots and replace any row? | Yes. `dsh --profile web --dump-config`; the docs promise “Any row it prints can be replaced by a patch of your own” (`architecture.md:39`) |
| **Everything the model sees is reconstructable** | Is there a runtime assertion? | Yes. `ctx.invariants` service + an `./invariant` companion plugin in 218 packages (`docs/subsystems/invariants.md`) |

### 1.4 Relation to earlier work in this series

```mermaid
mindmap
  root((Agent 架构的三种形态))
    循环型
      Claude Code
      Codex
      Cline
      特征：主循环是中心<br/>功能围着它长
    宿主型
      Open Design
      特征：不写循环<br/>把别人的 CLI 当引擎
    harness 型
      DeepSeek Harness
      特征：没有中心<br/>循环也是插件
      Reasonix 部分接近<br/>但仍是单循环
```

The same company’s two open-source agents happen to take two roads: **Reasonix** is “one static binary + cache-first + a transport-agnostic controller,” chasing cost and portability; **Harness** is “a 241-package monorepo + everything is a plugin,” chasing replaceability and extensibility. That contrast itself is interesting — Chapter 31 will expand it.

---

<h2 id="ch2">Chapter 2 · Evidence of motive: the original wording in README / AGENTS.md / the paper</h2>

### 2.1 Evidence table

| Evidence | Source | What it proves |
|---|---|---|
| “everything is a plugin” | `README.md:7` | The architectural stance is the first sentence, not a row in a feature list |
| “powered by Cordis … Spatiotemporal Composability” | `README.md:7` | The substrate has an independent paper; it is not a homemade toy |
| “There is no privileged core to patch” | `docs/architecture.md:13` | There is no center into which you can stuff code |
| “Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and **the agent loop itself**” | `docs/architecture.md:11` | The loop is a plugin, written in the body of the architecture document |
| “**Plugins, not loop changes**: changing `agent-loop` requires updating docs/architecture.md” | `AGENTS.md` | The bar for changing the loop is raised to “you have to change the architecture document” |
| “**Model-visible ⟺ logged**” | `AGENTS.md` | Observability is a constitutional constraint |
| “**Registrations are effects**” | `AGENTS.md` | Unwindability is a constitutional constraint |
| “A capability seam comprises Service Definition / Service Provider / Consumer roles. It is complete, never one role” | `AGENTS.md` | The smallest unit of a capability is three roles, not a class |
| “**Trust TypeScript at typed same-process boundaries.** Do not add runtime validation … solely for values the static interface requires” | `AGENTS.md` | Anti-over-defense — a rare rule |
| “per-file 100% on `packages/*/*/src`” | `AGENTS.md` / `docs/testing.md:10` | The coverage gate is per-file full marks |
| “**We are DeepSeek — do not ration real-API tests.**” | `docs/testing.md:19` | The test policy carries a company’s self-awareness |
| “Developer preview … **THERE WILL BE COMPATIBILITY-BREAKING CHANGES**” | `README.md:13` | A warning in capitals, and the constitution has a dedicated “pre-release stance” section |

### 2.2 A rare rule: refuse over-defense

Most engineering norms teach you to “validate more.” dsh does the opposite (`AGENTS.md`):

> **Trust TypeScript at typed same-process boundaries.** Do not add runtime validation, fallback behavior, or hostile-input tests solely for values the static interface requires; validate at parser/config, queued, model/tool JSON, durable/file, worker, process, and wire boundaries.

It lists **seven boundaries that must be validated** (parser/config, queue, model and tool JSON, durable/file, worker, process, wire) and explicitly says: do not add validation elsewhere.

The reason is a chain reaction of per-file 100% coverage: **if you add a defensive branch that cannot fire, you have to write an impossible test for it, or write an exemption comment**. So “over-defense” has a direct cost under this constitution.

This is a rule I have not seen a second house write down in this series. It shows that this repository’s norms are not a copied best-practice checklist; they grew out of its own gate system.

### 2.3 “Pre-release stance”: writing the technical-debt policy into the constitution

`AGENTS.md` has a section titled **"Pre-release stance: foundation over blast radius"**. The first sentence is:

> **Remove this section at the first tagged release.** With no external consumers, prefer the correct foundation over compatibility shims: rename or repackage freely and update every reference together. Backends reject old on-disk formats.

It even wrote its own deletion condition — **delete this section at the first official tag**.

This style of “temporary rule with a self-destruct instruction” is far more effective than the verbal promise “we will refactor later.” Accompanying it: `SESSION_FORMAT_VERSION` stays at `0` and **explicitly makes no compatibility promise**.

---

<h2 id="ch3">Chapter 3 · Macro stack: the numeric shape of 241 packages and the direction of dependence</h2>

### 3.1 Top-level shape

```text
deepseek-harness/
├── vendor/          # 9 包 · 6 584 行 —— pinned 的 Cordis 源码副本（manifest 带上游 SHA）
├── packages/        # 219 包 —— @deepseek-ai/dsh-<name>，按 group/pkg 两级组织
│   ├── core/        #   8 包 · 40 854 行 —— session · system-prompt · tools · agent · agent-loop · scope
│   ├── client/      #  39 包 · 138 163 行 —— Web 客户端模块（全仓最大组）
│   ├── subagent/    #  11 包 · 24 555 行 —— 子智能体缝 + 多种 provider
│   ├── host/        #   8 包 · 22 513 行 —— 宿主能力
│   ├── session/     #  13 包 · 21 520 行 —— 日志持久化/投影/标题/遥测
│   ├── llm/         #   5 包 · 20 502 行 —— 模型缝 + DeepSeek providers
│   ├── extensions/  #   4 包 · 20 305 行
│   ├── typert/      #  11 包 · 15 404 行 —— 类型图生成器/加载器/运行时注册表
│   ├── test-support/#   6 包 · 13 391 行 —— 测试基础设施（自成一等公民）
│   ├── fs/ shell/ sandbox/ terminal/ lsp/ … 执行世界 40 包 · 62 000+ 行
│   └── goal/ plan/ todo/ schedule/ jobs/ guard/ … 治理 39 包
├── apps/            #   2 包 · 23 928 行 —— cli（拥有 dsh bin）+ web
├── native/          #   4 包 · 343 行 —— Landlock 原生启动器（按平台分包）
├── python/          #   4 286 行 —— Python SDK + bundled runtime
├── examples/        # 可运行的 cordis.yml 叶子
├── docs/            # 40+ 子系统文档，全部双语，其中 8 177 行是生成的
├── scripts/         # 124 个 .ts —— 生成器 + 28 个 verify-* 门禁
├── .agents/         # 686 篇 Agent Notes + 11 个 skill
└── AGENTS.md        # 架构宪法（CLAUDE.md 是它的符号链接）
```

### 3.2 Micro stack: the layers one request actually walks

After the macro, the micro. The technical layers a model request actually walks:

```mermaid
flowchart TB
    subgraph L1["① 装配层（启动期，YAML → 插件树）"]
        A1["profile（web/headless）"] --> A2["bundle 有序叠层"]
        A2 --> A3["cordis.patch.yml × 3 级 + --patch"]
        A3 --> A4["Loader：!!js 表达式插值"]
    end
    subgraph L2["② 框架层（Cordis）"]
        B1["Context：服务仓库"] --> B2["inject：不动点解析加载顺序"]
        B2 --> B3["Typed Events：emit/waterfall/parallel/serial"]
        B3 --> B4["effect：可逆注册 + 逆序拆卸"]
    end
    subgraph L3["③ 主轴层（core 8 包）"]
        C1["ctx.sessions：append-only 日志"]
        C2["ctx.systemPrompt：有序段 + 工具 schema"]
        C3["ctx.tools：注册表 + 五段流水线"]
        C4["ctx.agents / ctx.agentLoop：驱动"]
        C5["ctx.scope：逐 agent 注册隔离"]
    end
    subgraph L4["④ 能力层（seam × N，每个三角色）"]
        D1["ctx.llm"] & D2["ctx.fs / subprocess"] & D3["ctx.shell / terminals / lsp"]
        D4["ctx.sandbox"] & D5["ctx.subagents"] & D6["ctx.compaction / spillStore"]
    end
    subgraph L5["⑤ 前端层（一套行为四种壳）"]
        E1["apps/cli"] & E2["apps/web + client 39 包"] & E3["ACP 自动化"] & E4["JSON-RPC SDK（TS/Python）"]
    end
    L1 --> L2 --> L3 --> L4
    L3 --> L5
    L4 -.被 L3 注入.-> L3
```

The five-layer division of labor is very clean: **the assembly layer decides what exists, the framework layer decides how it is wired, the spine layer decides how it runs, the capability layer decides what it can do, the frontend layer decides who is watching.**

### 3.3 In-degree: where the real center of gravity is

I ran a full census of the 241 packages (script in Chapter 33), counting inter-package dependencies in `package.json` and `inject` declarations. Packages with the highest in-degree:

| Most-depended-on package | Count | Note |
|---|---|---|
| **`dsh-invariants`** | **218** | **90%** of the 241 packages depend on it — because every package must publish an `./invariant` companion plugin |
| `schemastery` | 110 | vendored config validation (runtime schema for `Config` fields) |
| `dsh-session` | 84 | the session log is the truth; everyone has to read it |
| `dsh-llm` | 82 | message / streaming vocabulary |
| `dsh-agent` | 61 | Agent interface and inbox |
| `dsh-tools` | 47 | tool registry |
| `dsh-brand` | 30 | **branded id** (the constitution forbids a bare `string` for a cross-boundary id) |
| `dsh-system-prompt` | 30 | prompt registry |
| `dsh-timeout` | 23 | timeout policy |

**`dsh-invariants` at 218 in-degree** is the single most informative number in this analysis. It means “runtime invariants” are not an optional practice of some team; they are **part of a package’s birth certificate**. Chapter 27 will cover how this constitution operates.

Service-side in-degree (`inject` declarations):

| Most-injected service | Count |
|---|---|
| `tools` | 31 |
| `systemPrompt` | 20 |
| `sessions` | 17 |
| `agents` | 13 |
| `subagents` | 12 |
| `llm` | 9 |
| `subprocess` | 8 |

**`tools` 31 times, `systemPrompt` 20 times** means these two are the largest confluence points — half the repository is contributing into the tool table and the prompt. That is exactly why they must be **registries**, not constants.

### 3.4 The stack in one breath

- **Language**: TypeScript (ESM everywhere, `strict: true` + `noImplicitAny`; every residual `any` must explain why it cannot be narrowed)
- **Package management**: pnpm 11.7 workspaces, node `^22.19 || >=24`
- **Framework**: vendored Cordis (plugins / services / events / effects)
- **Build**: tsc emits lib/types + tsdown builds the runtime bundle; **source face and artifact face are strictly unmixed**
- **Tests**: vitest × 6 layers (unit / per-file 100% coverage / real-API e2e / keyless snapshot / browser snapshot / gate collection)
- **Frontend**: `apps/web` + `client` 39 packages (138 163 lines, including UI slots / primitives / locale / connection layering)
- **Native**: the Landlock launcher is a **C11** static executable (`native/landlock-run/packages/entry/src/main.c`), not an N-API addon; the npm name carrying `node-addon` is historical naming only. On Linux, `sandbox-local` tries **bwrap** first then falls back to Landlock; darwin uses **Seatbelt/`sandbox-exec`**; win32 uses a **windows-acl** restricted token (`PLATFORM_CHAINS` in `sandbox-local/src/index.ts`)
- **Protocol surface**: ACP (Agent Client Protocol), JSON-RPC SDK (TS + Python), Typert RPC gateway

---

<div class="part-band"><span class="band-k">Part II</span>Constitution: five ideas and three layers of order</div>

<h2 id="ch4">Chapter 4 · A developer mind map: every design follows from “everything is a plugin”</h2>

The entire repository’s design can be derived from one sentence. This diagram is the skeleton of the lecture:

```mermaid
mindmap
  root((everything<br/>is a plugin))
    没有特权内核
      Context 是服务仓库
      73 个 ctx 服务键
      循环也是插件<br/>改它要改架构文档
    空间可替换
      capability seam 三角色
      Definition 只声明接口
      Provider 可换
      Consumer 不知道底下是谁
      换两个 provider<br/>搬走五种能力
    时间可撤销
      registrations are effects
      每个 register 返回 disposer
      逆序拆卸
      HMR + 逐会话装配
    配置即架构
      profile 叠 bundle
      三级 patch + CLI overlay
      dump-config 每行可换
      一个 117 行的包<br/>实质是一个 YAML
    真相唯一
      append-only 会话日志
      deriveMessages 投影
      模型可见 ⟺ 已记录
      218 包的 invariant 伴生
    能改与只能看分开
      waterfall 可改写
      emit + Readonly + 冻结
      单调守卫只能拒
      dispatch mode 是公共契约
    纪律机械化
      逐文件 100% 覆盖
      28 个 verify 门禁
      686 篇 Agent Notes
      8177 行生成文档
```

Seven branches, each a necessary condition **back-derived** from the constraint “there is no center”:

- No center → consumers cannot import an implementation → **you need a service warehouse**
- Services must be replaceable → interface and implementation must separate → **you need the seam’s three roles**
- Plugins come and go → registration must be unwindable → **you need effect**
- Assembly decides behavior → assembly must be inspectable and overridable → **you need layered patches + dump-config**
- No center holds state → state must live in one shared truth → **you need an append-only log**
- Anyone can mount a listener → a safety boundary cannot rest on listeners → **you need monotonic guards**
- Too many rules, and collaborators include AIs → spoken convention will rot → **you need machine gates**

---

<h2 id="ch5">Chapter 5 · Cordis’s five ideas: spatially replaceable × temporally unwindable</h2>

### 5.1 Five sentences

`docs/cordis-primer.md:9-13` defines the entire framework in five sentences:

| # | Idea | Key sentence from the original |
|---|---|---|
| 1 | **A plugin is an object that implements a Service** | "a function with optional `inject` and `apply(ctx)` fields, or a `Service` subclass whose lifecycle Cordis mounts into the current context" |
| 2 | **context is a service warehouse** | "other plugins find services **via key instead of importing a concrete implementation**" |
| 3 | **Declare service dependencies with `inject`** | "**load order is expressed through service requirements rather than manual boot sequencing**" |
| 4 | **Typed Events communication** | "declare event names through TypeScript declaration merging, then dispatch them as `emit`, `waterfall`, `parallel`, or `serial`" |
| 5 | **Registration is a reversible effect** | "installed through `ctx.effect()` or `ctx.on()` so reload and teardown unwind them predictably" |

Item 3 is the turning point of the whole architecture. **“Load order is expressed through service requirements, not manual boot sequencing”** — that one sentence kills the nastiest class of complexity in agent frameworks: the boot-order table.

### 5.2 How a service key is declared: declaration merging

```ts
// packages/shell/shell/src/index.ts:40-44
declare module '@deepseek-ai/cordis' {
  interface Context {
    shell: ShellExecutor
  }
}
```

This move gets two things that are usually mutually exclusive:

- **Globally visible types**: writing `ctx.shell.run(...)` in any package has full type hints
- **Runtime-injected implementation**: no import of any concrete implementation

The glossary has a strict constraint on Service Definition (`docs/glossary.md:9`): it must be a Cordis `Service` (an abstract class such as `ShellExecutor`, or a concrete registry such as `WebRuntime`), **“never a TypeScript `interface`”**.

Why not an interface? Because an interface is type-only with no runtime existence, and a seam needs a **runtime entity** that can be `provide`d onto `ctx`, can hold a lifecycle, and can be torn down by HMR.

### 5.3 Duplicate service names: fail loud

The comment at `packages/shell/shell/src/index.ts:16-20` explains why the shell settings namespace belongs to the Definition and not to any executor:

> a host composes exactly one provider of `ctx.shell` (the win32 layer swaps the POSIX rows for the pwsh ones, and **mounting both fails loud on a duplicate service registration**), so the providers share one namespace without ever registering it twice, and a settings document carried between platforms keeps resolving on both.

Three payoffs in one: the Windows layer switches to PowerShell by **replacing config rows** rather than adding a branch; mounting two providers at once **explodes immediately** rather than silently picking one; a settings document carried across platforms still resolves on both sides.

### 5.4 Four dispatch modes and waterfall semantics

See Chapter 7.

### 5.5 Cost

| Cost | Concrete manifestation |
|---|---|
| Steep learning curve | Understanding one request requires simultaneous grasp of the service warehouse, inject resolution, four dispatch modes, and the effect lifecycle |
| Many layers of indirection | To know who `ctx.shell` actually is, you look at assembly config, not at code |
| Debugging needs new tools | Hence `--dump-config`, the `cordis_inspect_*` tools, `docs/module-graph.md` |
| Vendored dependencies must be maintained | `vendor/README.md` has a manifest + sync procedure + recorded local patches; every sync must re-run test and build |

Item 4 is worth noting: dsh **vendors** Cordis into the repository rather than taking it as an npm dependency. The vendoring policy in `AGENTS.md` requires: a pinned source copy, a manifest carrying the upstream SHA, updates through the sync procedure, **local patches must be re-applied or retired**, and `test && build` must be re-run after sync.

This is a pragmatic choice: framework and product are on the same evolution cadence; they are not yet at a stage where a version can be locked.

---

<h2 id="ch6">Chapter 6 · The capability seam: three roles, or it is not a seam</h2>

### 6.1 Motive and constraint

**Motive**: let a capability such as “execute bash” be replaceable as a whole (host / sandbox / remote / PowerShell), while **the tool contract the model sees stays unchanged**.

**Constraint**: once the schema the model sees changes, the prompt cache dies, model behavior drifts, snapshot tests all go red. So **replacement must happen on a layer the model cannot see**.

### 6.2 Rejected alternatives

| Rejected | Why |
|---|---|
| if/else inside the tool to pick an implementation | Every new implementation edits the tool; the tool becomes a catalog of implementations |
| A TypeScript `interface` as the interface | No runtime entity; cannot provide, cannot hold a lifecycle, cannot HMR |
| One package that is interface + implementation + tool | Swapping the implementation forks the whole package, tool included |

### 6.3 Choice: three roles

`docs/architecture.md:100`:

> A **seam** is a swappable capability with three roles: a **Service Definition** declaring the interface, a **Service Provider** implementing it, and a **Consumer** using it, commonly a model-facing tool. A package may combine roles, but **one role alone is not a seam**; adding a capability means designing all three.

`AGENTS.md` upgrades this to a hard rule:

> **A capability seam comprises Service Definition / Service Provider / Consumer roles.** It is complete, never one role; split only when roles evolve independently.

shell is the officially designated template (`docs/glossary.md:9`):

```mermaid
flowchart LR
    subgraph D["① Definition · dsh-shell（476 行）"]
        DD["abstract class ShellExecutor<br/>declare ctx.shell<br/>词汇：ExecRequest / ExecSpec / Process / RunResult<br/>+ SHELL_SETTINGS_NAMESPACE"]
    end
    subgraph P["② Providers（四个）"]
        P1["bash-local"]; P2["bash-sandbox"]; P3["pwsh-local"]; P4["pwsh-sandbox"]
    end
    subgraph C["③ Consumers（三个模型可见工具）"]
        C1["tool-bash → bash"]; C2["tool-pwsh → pwsh"]; C3["tool-bash-persistent → bash（PTY）"]
    end
    DD -.实现.- P
    DD -.注入.- C
    C --> M["模型只看到工具名 + JSON Schema"]
    P --> W["ctx.subprocess / ctx.terminals"]
```

### 6.4 A refined detail: the request/spec split

`AGENTS.md` designates shell as the template for “explicit over implicit”:

> **Explicit > implicit at package boundaries**: defaulting is an explicit `resolve(request): Spec` step in the owning implementation, **never a hidden `?? default` inside `run()`**（the `dsh-shell` request/spec split is the template）.

`ShellExecRequest` (optional fields) and `ShellExecSpec` (complete fields) are two types. Default resolution is an **explicit step that can be logged and asserted**, not a `??` hidden inside `run()`.

The cost of not doing this is concrete: the caller does not know the actual timeout, it cannot be found in the log, it silently changes after a provider swap, tests cannot assert it.

### 6.5 The Definition’s sense of boundary

The module JSDoc at `packages/shell/shell/src/index.ts:1-5`:

> Service Definition for the `ctx.shell` capability seam, covering foreground commands and background process handles. **Job ids, ownership, polling, and notices belong to `@deepseek-ai/dsh-jobs`, keeping executors independent of sessions.**

**“Keep executors independent of sessions”** — background-job ids, ownership, polling, and notices all belong to `ctx.jobs`. So the same executor can serve chat, an automation protocol, and a sub-agent at once.

This sense of “push out what is not mine” appears repeatedly in the module JSDoc of 241 packages. It is why this architecture can grow this large without collapsing.

### 6.6 Cost

- **A capability is at least three packages.** The repository’s package count therefore inflates to 241.
- **Navigation cost**: to change “bash timeout” you first have to decide whether to change the Definition (contract), the Provider (implementation), or the Consumer (schema).
- **Risk of over-splitting**: the constitution writes “split only when roles evolve independently,” and allows one package to hold multiple roles (`dsh-llm` is both Definition and Consumer).

---

<h2 id="ch7">Chapter 7 · Three event domains and four dispatch modes: “may mutate” is written into the type</h2>

### 7.1 Three event domains

`docs/architecture.md:55` is blunt: **"Events are the extension points, and picking the right domain is the first decision in most changes."**

| Domain | Semantics | When to use |
|---|---|---|
| **Session events** | **Durable facts** appended to the log, broadcast through `session/event` | This fact must survive reload |
| **Agent events** (`agent/*`) | Carry a live `Agent`: inbox / step / status / request / validation / continuation | Observe or **intercept** in-flight work |
| **Capability events** (`fs/*`, `tools/*`, `telemetry/*`) | Hang policy and adapters onto a seam | Add policy **without importing the loop** |

The complete producer/consumer table for the three domains is in `docs/event-producer-consumer.md` (generated).

### 7.2 Four dispatch modes

`docs/cordis-primer.md:19-24`:

| Mode | await? | Order | Has a return value? | Use |
|---|---|---|---|---|
| `emit` | no | registration order | no | observe |
| `waterfall` | no | registration order | **yes** | rewrite, intercept, policy |
| `parallel` | yes | parallel | no | fan-out |
| `serial` | yes | registration order | yes | sequential statements |

The key sentence (`:26`):

> **The dispatch mode is part of the event's public contract.** New harness events document it with an `@mode` tag so the generated catalog can check declarations against dispatch sites.

**The dispatch mode is a public contract**, written in JSDoc with an `@mode` tag, and **the generated catalog checks that declarations match actual dispatch sites**.

### 7.3 Technical highlight: separating “may mutate” from “may only watch” with types

Look at the tool registry’s four events (`packages/core/tools/src/index.ts:150,161,173,195`):

```ts
/** @mode waterfall */ 'tools/pre-execute'(exec, next): Promise<PreToolDecision>
/** @mode waterfall */ 'tools/execute'(exec, next): Promise<…>
/** @mode waterfall */ 'tools/post-execute'(exec, result, next): Promise<PostToolDecision>
/** @mode emit      */ 'tools/result'(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
```

The first three are waterfall (**may rewrite**); the last is emit + both parameters `Readonly<>` (**may only observe**).

And it does not rest on types alone — there is also a runtime invariant (`packages/core/tools/src/invariant.ts:23-25`):

```ts
if (!Object.isFrozen(exec)) fail('tools/result execution must be frozen before publication')
if (…) fail('tools/result outcome and content must be frozen before publication')
```

**Once a result is finalized it is frozen; no observer can mutate it; a violation is caught at runtime.**

This is the most meticulous treatment of an “event system” I have seen in this series. In most projects’ EventEmitters, “some listener secretly mutated a finalized result” is a class of extremely hard-to-debug bugs; dsh uses dispatch mode + `Readonly` + `Object.freeze` + a runtime assertion as four layers of defense to make it impossible.

### 7.4 waterfall is around-middleware

`docs/cordis-primer.md:30`:

> `ctx.waterfall` is around-middleware. A listener receives `(...args, next)`. Call `next()` to delegate the possibly wrapped result to the next service; **return without `next()` to short-circuit.**

Paired with the capitalized rule in `AGENTS.md`:

> **Waterfall listeners MUST call `next()`** to delegate; returning without it short-circuits the chain.

Short-circuit **is part of the design** (`:34`):

> For single-decision events, short-circuiting is the design. A policy listener can return without `next()` when it owns the decision, while a listener that only annotates or observes must delegate.

The danger of this rule is that **violating it does not error**: a listener that only wanted to log forgets `next()`, every downstream policy silently dies, and the logs look fine. That is why it is lifted into `AGENTS.md` with a capitalized MUST.

---

<div class="part-band"><span class="band-k">Part III</span>Spine: loop, log, tools</div>

<h2 id="ch8">Chapter 8 · Turn and step: one loop, two boundary levels</h2>

### 8.1 Definitions

`docs/architecture.md:65`:

> A **step** is one model request plus the tools it calls. A **turn** is zero or more steps: it **opens before its first input is claimed** and **closes once nothing is owed**.

Two deliberate phrasings:

- **"zero or more steps"** — a zero-step turn is legal (see 8.4)
- **"once nothing is owed"** — the close condition is “nothing is owed,” not “the model finished speaking”

### 8.2 The full pipeline

The architecture document gives a stretch of pseudocode (`:67-82`). I mapped it against the source into a sequence diagram:

```mermaid
sequenceDiagram
    participant IN as inbox
    participant D as ReactLoopAgent
    participant SP as ctx.systemPrompt
    participant L as 会话日志
    participant M as ctx.llm
    participant T as ctx.tools

    D->>L: turn/start
    loop 每个 step
        D->>IN: claim(target, turn)
        D->>SP: assemble(assembleContextFor(agent, signal))
        D->>D: agent/pre-step ⟨waterfall⟩ → reject | enter(messages)
        Note over D: reject 或首个 enter 被改空<br/>→ 关掉零 step 的 turn（日志留痕）
        D->>L: step/start
        D->>L: user/message ×N
        D->>L: deriveMessages()
        D->>D: agent/request ⟨waterfall⟩ → provider/model/参数
        D->>M: prepareCall → stream(request)
        M-->>L: assistant/chunk ×N（每片记 seq）
        D->>L: assistant/message（sourceEventSeqs 回指 chunk）
        alt finish = error/aborted
            D->>D: agent/request-error ⟨waterfall⟩ → retry?
        else max-tokens
            D->>D: 返回 max-tokens（sticky）
        else 无 tool-call
            D->>D: completed
        else 有 tool-call
            D->>T: executeToolCalls（三道 waterfall）
            T-->>IN: 附加上下文 → next-step 队列
        end
        D->>L: step/end
        D->>D: agent/turn-stopping ⟨serial⟩
        Note over D,IN: 再查 inbox.nextStep：非空则继续
    end
    D->>L: turn/end（结构化 reason）
```

Source locations: the outer `while (await this.turn()) {}` is at `packages/core/agent-loop/src/agent.ts:212`, `turn()` at `:246`, `step()` at `:332`, `buildRequest()` at `:407`.

### 8.3 Four technical highlights

**① `max-tokens` is sticky** (`agent.ts:285-290`):

> max-tokens is sticky: once any step hits the ceiling, later steps that complete normally **must not downgrade the turn outcome**.

If step 3 of a turn hits the output ceiling and step 4 finishes normally — the turn’s conclusion must still be “hit the ceiling.” Downstream (UI hints, retry policy, eval) uses that to judge whether the result is trustworthy. **Downgrading it is lying.**

**② Every chunk enters the log, and the message points back at its sources** (`agent.ts:349,381-390`):

```ts
chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
// …
this.session.append('assistant/message', { turn, step, message, usage },
  { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })
```

`sourceEventSeqs` lets any final message be traced back to the raw streaming slices that composed it. When you are debugging “why was this answer truncated,” this is life-saving.

**③ Request errors are a retryable waterfall, not try/catch** (`agent.ts:354-371`):

```ts
const action = await this.dispatch.waterfall('agent/request-error',
  { turn, step, provider, failure, retryPolicy, signal }, () => Promise.resolve(undefined))
if (action?.kind !== 'retry') throw new LlmError(…)
continue    // 重试整个请求（包括重新 buildRequest）
```

**Retry policy is a plugin**, not a constant in the loop. And a retry re-walks `buildRequest` — so a retry can swap provider/model.

**④ Failure is structured** (`agent.ts:307-314`):

```ts
turnEnds = { kind: 'error', error: error instanceof LlmError
  ? error.failure                                     // LlmError 保留它的事实
  : { message: errorChain(error), code: 'UNKNOWN' } }  // 其它一切 flatten
```

`turn/end` is always emitted in `finally` (`:316-322`). **Never let a half-turn appear in the log** — otherwise replay logic has to handle incomplete state, and that is a bottomless pit.

### 8.4 Why a zero-step turn must exist

```ts
// agent.ts:271-277
if (turnEnds && decision.messages.length === 0) break
// A removed waking message or an enter decision rewritten to empty
// still owns the initial turn boundary, but it spends no model call.
if (phase.step === 0 && decision.messages.length === 0) {
  turnEnds = { kind: 'completed' }
  return false
}
```

The user pressed send, but some plugin emptied the message. A turn is still opened and closed normally. architecture.md explains (`:88`):

> a rejected or empty first claim still closes a durable turn that spent no step, **so the log records the attempt**.

The consequence of not doing this: the UI shows “I clearly sent a message but nothing happened,” and the log has no trace. **“The user initiated an attempt” is itself a fact that needs to be recorded.**

### 8.5 A constitutional fingerprint: `/* v8 ignore next */`

`agent.ts:333`:

```ts
/* v8 ignore next -- private callers establish the running phase before executing a step */
if (this.phase.kind !== 'running') throw new Error(`agent "${this.id}": step outside running phase`)
```

This is a fingerprint left by the **per-file 100% coverage gate**: an unreachable defensive branch must be explicitly exempted **and the reason written down**. And `scripts/coverage-exempt.ts` + `coverage-exempt.spec.ts` in turn guard the exemptions themselves. See Chapter 26.

---

<h2 id="ch9">Chapter 9 · Inbox and `agent/turn-stopping`: keep the loop going without changing the loop</h2>

### 9.1 One inbox, two delivery slots

`docs/architecture.md:86`:

> Input reaches the driver through one inbox. Some messages wake it immediately; **injected context waits in the inbox until another message does.**

Four input sources, two wake semantics (`packages/core/agent-loop/src/agent.ts:122-132`):

| API / source | Wake? | Delivery slot |
|---|---|---|
| `followup` / user message | immediately | **next-turn** |
| Additional context produced by a tool | not needed (inside the current turn) | **next-step** |
| `steer` | immediately | **next-step** |
| `inject` | **does not wake** | **next-step** (claimed at the current or next already-woken step boundary) |

`inject` and `steer` enter the same `next-step` queue; the difference is only wake. **Injection does not open a turn on its own**: when idle it only enters the inbox; when running it is eaten only at a step boundary **after the already-claimed batch**. Otherwise every injected “the current branch is main” would idle-open a round.

The inbox is not a pure in-memory queue. First `session.append('agent/inbox/spliced', …)`, then mutate the live projection (`packages/core/agent/src/inbox.ts:157-193`). cancel / resume / fork can all reconstruct pending work from the log.

`target` switching inside the loop (`agent.ts:261,300`):

```ts
let target: InboxTarget = 'next-turn'   // turn 的第一个 step 领 turn 队列
// …一个 step 结束后…
target = 'next-step'                    // 后续 step 领 step 队列
```

### 9.2 How tools stuff things into the inbox

`agent.ts:395-398`:

```ts
const { concluded } = await executeToolCalls(
  this.loopCtx, turn, step, toolCalls, signal,
  context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]),
)
return concluded ? { kind: 'completed' } : null
```

Two details:

- The sixth argument is a delivery callback — this is where the `tools/post-execute` “add context” decision lands. The pipeline document describes it as “Active-batch additionalContexts FIFO → injected user/message **after recorded tool results**”.
- `concluded ? completed : null` — **“whether another request is still owed” is reported by the tool batch itself**, not guessed by the loop.

### 9.3 Technical highlight: the “announce–object” pattern of `agent/turn-stopping`

The two checks before the loop wants to stop (`agent.ts:295-299`):

```ts
if (turnEnds && this.inbox.nextStep.length === 0) {
  await this.dispatch.serial('agent/turn-stopping', { turn, signal })   // 发公告
  signal.throwIfAborted()
}
if (turnEnds && this.inbox.nextStep.length === 0) break                 // 再查一次
```

After the serial event is sent, **check the inbox again**. Because a listener is fully allowed to stuff something new into the inbox at that moment.

This is the textbook answer to **“keep the loop going without changing the loop”**: give the loop an announcement moment of “I am about to stop,” and any plugin can object at that moment.

`agent/turn-stopping` is **serial and has no `next()`** (`architecture.md:84`) — every listener is awaited in turn; no one can short-circuit someone else’s right to object. The standard objection from a listener is **`steer`** (stuff a message into `next-step` and wake), then the second inbox check is non-empty, and the turn continues. This is the answer to “keep the loop going without changing the loop” **inside the same turn**.

**Goal continuation in the same session is not this hook.** `goal-round-driver` listens for `agent/status` entering idle with the goal still armed, then `agent.followup` opens **the next turn** (see Chapter 14). Writing goal continuation into `turn-stopping` is wrong: that would mash “this round still owes a step” and “the goal is not finished, open another round” into one thing.

### 9.4 Wake latch: teardown waits on no model turn

`wakeDriver` at `agent.ts:164-193`:

```ts
if (this.phase.kind !== 'idle') {
  // Maintenance and aborted drivers cannot deliver the wake: latch it for
  // replay at convergence. Live drivers claim queued work themselves;
  // disposal never latches, so teardown waits on no model turn.
  const reason = this.phase.abort.signal.reason as AgentCancelCause | undefined
  if (reason?.kind !== 'disposed' && (this.phase.kind === 'maintenance' || wakeAfterAbort)) {
    this.phase.wakeRequested = true
  }
  return
}
```

`Phase` is three states: `idle` / `maintenance` / `running` (`agent.ts:38-46`). The **maintenance phase** is a design rarely seen elsewhere — while the driver is doing maintenance work (compaction, title generation) it does not accept wake delivery; it latches and waits for convergence.

**And disposal never latches**: `so teardown waits on no model turn`. This class of detail — “teardown must not be held hostage by business logic” — is the watershed between a harness that has run in production for a long time and a demo.

---

<h2 id="ch10">Chapter 10 · “Model-visible ⟺ logged”: turning the constitution into a runtime assertion</h2>

### 10.1 Motive

In the step-0 scheme of `const messages = [...]`, the model’s input lives in memory. Then reconnect after disconnect, session fork, auditing “what did the model actually see,” UI replay of the streaming process, eval reproduction — none of it can be done.

### 10.2 Choice: the log is the truth, messages are a projection

`docs/architecture.md:94`:

> The session log is the source of the context the model sees. `deriveMessages()` projects model history from it, and raw `assistant/chunk` events preserve replay and UI fidelity. **Fork, resume, transcripts, telemetry, and persistence all derive from this stream.**

The direction is crucial: not “the message array happens to be logged,” but “**the log is the truth, the message array is a projection**.”

The module JSDoc of `packages/core/session`:

> Event-sourced session service: append-only session log, in-memory store, and the derived LLM message history. **Persistence is a plugin concern** (subscribe to `session/event`, drain on `session/flush`).

**Persistence is a plugin concern** — the session service itself never touches disk. That is why the session group has 13 packages: log core + JSONL/zstd persistence + projection + title generation + telemetry + reference resolution…

### 10.3 Technical highlight: institutionalizing invariants

`docs/architecture.md:96` and `AGENTS.md`:

> **Model-visible means logged.** Anything that reaches a model request must be reconstructable from the log, **and a runtime invariant asserts it**. This is why a new model-visible input requires a new session event: extend `SessionEventMap` and render from the log.

The concrete assertion hangs on the `llm/stream` entry (`packages/core/agent-loop/src/invariant.ts:21-52`): for requests emitted by agent-loop, freeze `GenerateOptions`, then require JSON equality between `session.deriveMessages()` and `options.messages`, otherwise fail (`log-reconstruction desync`). The `agent/request` waterfall **cannot change messages**; to change history you can only append a session event. Without this: a plugin can silently mutate the outgoing array, live and replay fork, and there is no assertion.

Invariants are not a global assert function; they are a **per-package ownership registration service** (`docs/subsystems/invariants.md`):

```ts
// packages/runtime-diagnostics/invariants/src/index.ts:94
register(packageName: string, installer: InvariantInstaller): () => void
```

Constitutional points:

| Mechanism | Detail |
|---|---|
| **One companion plugin per package** | Every workspace package publishes `./invariant` and registers under **its own full npm package name** |
| **Violations are attributable** | Throws `InvariantError`, stable `code: 'INVARIANT'` + `packageName`, message prefix `invariant violated by "<package>": …` — **the registry imports no product package** |
| **Sub-fiber isolation** | The installer runs in a dedicated Cordis sub-fiber; `installer.inject` declares which services that fiber can access |
| **Name reservation** | Even if a filter turns the check off, the package name is still reserved — **two plugins can never silently steal the same name** |
| **Atomic failure** | If the installer fails, the sub-fiber is disposed and the reservation is released, atomically |
| **Config fails loud** | Regex allow/deny lists, deny beats allow; empty strings / whitespace / duplicates / invalid regex **throw at service start rather than skip** |

The most meticulous part is **what it specifies a check may assert** (`AGENTS.md`):

> **Runtime invariants assert owned relationships.** Check authoritative event streams or mutable data, **not** service or method presence, plugin metadata or effects, or fixed pure examples. **Without a plausible relationship, an explained empty companion is correct.**

You may not write an assertion that **pretends to check** such as “does `ctx.shell` exist.” When there is no real relationship to assert, the correct move is to export an empty installer **and explain why in a comment that starts with `No runtime invariant:`**.

And this too is machine-guarded: `pnpm run verify-package-invariants` rejects “generated placeholder markers,” “unexplained empty installers,” “non-empty installers that ignore the reporter,” “wrong registration names,” “incomplete export/publish/dependency/bundle wiring.”

**This is why `dsh-invariants` is depended on by 218 packages.** None of the 21 projects in this series does this.

### 10.4 A refined design in the session-log versioning mechanism

`AGENTS.md`:

> A `SessionEventMap` member is **required-on-read by default** — builds that do not know its type **refuse the log** unless the event carries the envelope's `ignorable: true`; only structural format changes bump `SESSION_FORMAT_VERSION`.

Strict by default: an old-version program that reads an unrecognized event type **refuses the entire log**, rather than skipping it and continuing. Because skipping means “a piece of model-visible history is missing,” and that produces silently wrong replay.

To let an event be ignorable by old versions, you must **explicitly** mark `ignorable: true` on the envelope. **Safe by default, exceptions explicit.**

### 10.5 Cost

- Any “add some context for the model” requirement must first extend `SessionEventMap` + add rendering, much more work than pushing a message
- Log volume is large (every chunk is recorded) — hence JSONL + zstd persistence and a packed-row layout
- 218 packages’ invariant companions are a real maintenance burden (even though most are empty installers with an explanation)

---

<h2 id="ch11">Chapter 11 · The tool pipeline: three waterfalls + monotonic guards + frozen results</h2>

### 11.1 Motive

Tool execution is the place in an agent that most needs plug-and-play: permission, approval, sandbox, timeout, retry, metrics, result rewriting, UI rendering, filesystem read/write policy, hook bridging… stuffing all of that into `runTool()` is a thousand-line mud ball.

The first sentence of `docs/tool-execution-pipeline.md` states the design goal:

> This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run **without changing the loop**.

### 11.2 The full pipeline

The module JSDoc of `packages/core/tools` summarizes it as five stages:

> Tool registry, model presentation modes, and **pre/guard/around/post/result** execution pipeline.

```mermaid
flowchart TD
    A["assistant 消息含 tool-call"] --> B["session: tool/call<br/>（执行前先记）"]
    B --> C["UI: presentCall(args)<br/>待处理卡片"]
    B --> D["tools/pre-execute ⟨waterfall⟩<br/>hooks · permission · sandbox"]
    D -->|allow| E["注册的单调守卫<br/>deny 或弃权 · 身份受保护"]
    D -->|deny| X["跳过工具体"]
    D -->|ask| F["ctx.approval 一次性询问<br/>缺失或无法回答 = deny"]
    F -->|allowed-once| E
    F -->|拒绝/取消/不可用| X
    E -->|allow| G["tools/execute ⟨waterfall · around⟩<br/>timeout · retry · metrics"]
    E -->|deny| X
    G --> H["ToolDefinition.execute() 本体"]
    H --> I["fs/write-intent · fs/edit-intent 门<br/>（仅 tool-fs 变更）"]
    H --> J["工具自有 session 事件<br/>todo/write · fs/observed · hook/* · tool/code-dispatch"]
    H --> G
    G --> K["tools/post-execute ⟨waterfall⟩<br/>accept · block · replace · add context"]
    X --> K
    K --> L["注册表外层规范化<br/>流水线/快照抛出 → isError"]
    L --> M["finalizeContent<br/>最后的 content-only 不变量"]
    M --> N["tools/result ⟨emit · 冻结⟩<br/>权威结果，只能观察"]
    N --> O["session: tool/result<br/>单一模型可见结果"]
    O --> P["additionalContexts FIFO<br/>→ inbox.next-step"]
```

### 11.3 Technical highlight one: monotonic guards

There is a design in the pipeline that looks redundant: given the `tools/pre-execute` waterfall, why an extra guard layer?

Because waterfall has a fatal flexibility: **a later-registered listener can override earlier decisions**.

```ts
// 一个能通过 code review 的"善意"插件
ctx.on('tools/pre-execute', async (exec, next) => {
  const d = await next()
  if (d.kind === 'deny' && isProbablySafe(exec)) return { kind: 'allow' }  // 提权了
  return d
})
```

So dsh runs a guard layer **after** the extensible waterfall. The pipeline document’s description:

> Registered monotonic guards — **deny or abstain; identity protected**

**Only two options: deny, or abstain. There is no “allow” return value**, so it is mathematically impossible to relax anything. Source JSDoc (`packages/core/tools/src/index.ts:704,1101`):

> A monotonic execution guard evaluated **after** every `tools/pre-execute` …
> Register a monotonic guard **after the extensible** `tools/pre-execute` …

The phrasing **“after the extensible”** is precise: the extensible part runs first, the non-relaxable part runs after. **Order itself is the safety design.**

| | waterfall | monotonic guard |
|---|---|---|
| Can rewrite someone else’s decision | yes | **no** |
| Order-sensitive | yes (later wins) | no (any deny is final) |
| A new plugin can accidentally privilege-escalate | **yes** | **impossible** |
| Fits | policy collaboration, context supplementation | **safety boundary** |

### 11.4 Technical highlight two: pipeline order itself is a runtime contract

`packages/core/tools/src/invariant.ts:94-115`:

```ts
if (eventName === 'tools/pre-execute') {
  if (stages.has(exec)) fail('tools/pre-execute repeated for one execution')
}
if (…) { if (stages.get(exec) !== 'pre') fail('tools/execute must follow tools/pre-execute') }
if (eventName === 'tools/post-execute') {
  if (…) fail('tools/post-execute must follow tools/pre-execute or tools/execute')
}
```

**Stage order is not implied by code structure; it is explicitly asserted by a runtime invariant.** A miswritten plugin that bypasses stage order explodes immediately and is attributed to its package name.

### 11.5 Technical highlight three: UI render intent is part of the design

`AGENTS.md`:

> **A tool's UI render intent is part of its design**, decided up front (`generic`/`terminal`/`diff`, `locations`); **presentation methods are pure functions of `args`**.

When a tool is defined it must already say “I should be rendered as an ordinary card, terminal output, or a diff.” And the render function must be a **pure function of `args`** — so a pending card can be drawn **before execution** (`presentCall(args)` in the pipeline diagram is before `tools/pre-execute`).

### 11.6 The catalog of 47 tools is booted, not statically scraped

The file-header comment of `docs/tool-catalog.md` (1 873 lines):

> Unlike the cordis catalog (a pure source-AST pass), this generator **BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`**, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). **A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented.**

A tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw JSON-Schema from MCP), so the generator **actually boots each tool plugin** to read the schema. And there is a completeness guard: **a new tool cannot be silently omitted from the docs**.

The 47 model-visible tools (from my census):

```
ask_user_question · bash · pwsh · console · edit · read · read_image · write · str_replace_editor
glob · grep · lsp · web_fetch · web_search · skill · todo_write · ralph · workflow
create_goal · get_goal · update_goal · schedule_create · schedule_delete · schedule_list
job_kill · job_list · job_output · terminal_open/read/send/close/list/signal
subagent · subagent_fork · send_message · interrupt_agent · list_agents · report
session_search · session_trace · session_event_read/search/trace
cordis_define/undefine/run/stop/inspect_list/inspect_query/inspect_self
```

That last line is the protagonist of Chapter 29: **the agent can define, run, and inspect its own Cordis plugins**.

---

<h2 id="ch12">Chapter 12 · System prompt and scope: reassemble every step, isolate per agent</h2>

### 12.1 systemPrompt is a registry, not a template

The module JSDoc of `packages/core/system-prompt` (1 582 lines):

> Registry for **ordered system sections, dynamic context, tool schemas, and prompt variables**.

Four kinds of contribution. Depended on by 30 packages, `inject` in-degree 20 — **half the repository has to stuff things into the prompt**, which is exactly why it must be a registry.

Assembly happens on **every step** (`agent.ts:230`):

```ts
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
```

Not once per turn. Because the tool set can change mid-turn: a sub-agent came up, plan mode exited, `cordis_run` mounted a new tool (Chapter 29).

`assembleContextFor(this, signal)` passes in both “which agent is asking” and the cancel signal — **assembly is per-agent**.

### 12.2 scope: two levels, flat, no inheritance

The module JSDoc of `packages/core/scope` (1 215 lines):

> Scoped-context primitive: mint a Cordis context that **tags registrations with an opaque identity** and build **routing-only event carriers** for that identity.

The glossary’s definition is stricter (`docs/glossary.md:13`):

> **scope** — the unit of per-agent registration: a contribution (tool, prompt section, variable, restriction, listener) is either *global* (visible to every agent) or *scoped* (owned by exactly one scope key). **Two levels, flat: scoped registrations do not inherit down to subagents; subtree behavior is expressed with lineage data, never scope structure.**

Three deliberate restrictions:

1. **Only two levels** (global / a single scope), no multi-level nesting
2. **No downward inheritance** — a sub-agent does not automatically receive the parent agent’s scoped registrations
3. When subtree behavior is needed, express it with **lineage data**, **never with scope structure**

Why this restraint? Because “scope inheritance” is the easiest place for a permission system to go wrong. Once inheritance is allowed, you have to answer “can a child override the parent’s restriction,” “what about a grandchild,” “what after a fork.” Two-level flatness kills all of those questions.

An instance (`dsh-tool-subagent-report` in `docs/tool-catalog.md`):

> Registered **per continuable in-process child** rather than globally, so this schema is visible **only inside such a child** and **survives its global `toolFilter`**.

The `report` tool is visible only inside a continuable in-process child, and **can pass through the global tool filter** — because it is the only channel a child has to report to the parent, and it must not be filtered out.

---

<div class="part-band"><span class="band-k">Part IV</span>Context economics and task governance</div>

<h2 id="ch13">Chapter 13 · Four service keys for one concern: compaction / spill / pruner / tokenMeter</h2>

Context management is one function in most projects. dsh splits it into **four independent service keys**, because they solve four different problems.

### 13.1 Problem decomposition

| Service key | Problem | Packages |
|---|---|---|
| `ctx.compaction` | **The conversation is too long**: after 20 rounds history exceeds the window | `packages/compaction` (4 packages · 8 032 lines) |
| `ctx.spillStore` | **A single output is too large**: grep matched 30 000 lines | `packages/spill` (3 packages · 1 473 lines) |
| `ctx.toolResultPruner` | **Tool results need pruning**: keep structure, delete redundancy | on the core/tools side |
| `ctx.tokenMeter` | **Need to know how much was spent**: metering and budget | `docs/subsystems/token-meter.md` |

A project that only does the first (compress history) will blow the window on the second case. A project that only does the second (truncate output) will never let the model see the truncated part.

### 13.2 spill: persist + locator + retrieval guidance

The Definition JSDoc of `packages/spill` (original wording captured by the census):

> Service Definition for the spill storage capability seam (`ctx.spillStore`): an abstract service defining WHAT a spill backend does — **persist a tool's oversized text and return a model-facing locator plus retrieval guidance**.

Three words are the key: **persist + locator (model-facing) + retrieval guidance**.

Not simple truncation: the full content is stored, and the model is given a message of “I stored the complete result here; you can read it this way.”

Real usage (`dsh-tool-fs-search` in `docs/tool-catalog.md`):

> Capped results save the complete formatted list through the optional `ctx.spillStore` backend; **returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments**.

**Spilled content is not a grave** — what the locator points at can still be processed by `read` and `grep`.

### 13.3 Compaction’s hard part is not the summary; it is “does this count as model-visible”

By Chapter 10’s constitution: a compaction result **does** count as model-visible, so it must be a session event, not a variable in memory.

That brings three payoffs: compaction is **auditable** (you can see which range was compacted and what the summary is), **replayable** (replaying the log yields the same model input), **unwindable** (delete that event and the original is restored).

The cost is that compaction cannot “mutate the array in place”; implementation complexity is higher.

### 13.4 `ctx.spillStore` is optional

Note the phrase above, “the **optional** `ctx.spillStore` backend.” Tools still work without a spill backend (they just truncate); with a backend they automatically upgrade to “truncate + locator.”

This is a secondary payoff of seam design: **absence of a capability is a legal deployment shape**, not a boot failure. Contrast `ctx.lsp`’s handling (`docs/tool-catalog.md`):

> Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; **without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema.**

**Without a provider, return a structured error, do not change the schema.** Because changing the schema kills the prompt cache and drifts model behavior. **The contract the model sees must be decoupled from deployment details.**

---

<h2 id="ch14">Chapter 14 · goal / plan / todo / schedule / jobs: “no free-form judgment” as plugins</h2>

### 14.1 goal: event-sourced + CAS + human root authority + bounded

The module JSDoc of `packages/goal` (4 packages · 5 786 lines · `ctx.goals`):

> Same-session goal domain: **event-sourced state, compare-and-set mutations, and process-local continuation activation**.

The deployment note in the tool catalog (`dsh-tool-goal` in `docs/tool-catalog.md`) is extremely dense:

> `create_goal`, `get_goal`, `update_goal` … **create, edit, pause, and resume require direct-human root authority**; complete and blocked **also accept the exact current goal round**. **The default blocked lower bound is three admitted rounds.**

Three designs, unpacked one by one:

**① Create / edit / pause / resume require “direct-human root authority.”** The model cannot assign itself a goal, nor quietly change a goal. Otherwise “make the tests green” gets rewritten to “comment the tests out.”

**② `complete` / `blocked` must carry the exact current round.** This is compare-and-set: the model says “I completed the goal of round 5”; if it is actually round 6, the claim is invalid. **This guards against stale claims.**

**③ `blocked` has a lower bound: at least 3 admitted rounds.** The model cannot say “I am stuck” on round one; it must actually have tried three rounds.

Continuation does not go through `agent/turn-stopping`. `goal-round-driver` (`packages/goal/goal-round-driver/src/index.ts:137-205`) renders `<goal_round>` and `agent.followup` when the agent is **idle**, the goal is **active and armed**, and the inbox has no competing work. After resume the durable phase is still there, but **activation is process-local state, disarmed by default**, to avoid auto-continuation on a cold start. Not one line of loop code is changed: the driver only consumes the public `agents` / `goals` / `sessions`.

### 14.2 plan: making plan mode logged state

In `AGENTS.md`’s repository layout, plan is described as **"plan mode as logged state"**.

Compared with Claude Code’s plan mode (a session-level boolean switch + prompt change), dsh’s approach is a `ctx.planMode` service + log events. The tool catalog’s note on `dsh-plan-mode`:

> `exit_plan_mode` **stays in the model-facing schema while planning is inactive** so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path **rejects calls outside plan mode**; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and **approval logs plan mode inactive at the step boundary**.

Three points of care:

- **The tool stays in the schema even outside plan mode** — because entering and leaving plan mode should not change the tool table the model sees (cache-friendly + snapshot-stable)
- The execute path **rejects** calls outside the mode — schema stability is not behavioral relaxation
- After approval, “plan mode inactive” is logged **at the step boundary** — the state change has a definite log moment

### 14.3 schedule: a versioned timing capability

`dsh-schedule` in `docs/tool-catalog.md`:

> Registered **only inside live root Agent scopes created after the opt-in Schedule plugin loads**. **Version 1** accepts `after_seconds`, explicit absolute `at`, and **bounded** fixed-rate `every_seconds`, and **discloses session-local delivery**; management reads and mutations require the shared Session persistence barrier.

The phrasing **“Version 1”** is worth noticing — it treats “this tool’s capability set” as a versioned contract, not an arbitrarily growing parameter table. `every_seconds` is **bounded** (you cannot set it to once per millisecond). And it **explicitly discloses** that delivery is session-local (not cross-session).

### 14.4 jobs: a kind-agnostic background-job controller

`dsh-tool-jobs` in `docs/tool-catalog.md`:

> The **kind-agnostic** background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed **through the same three tools**. Loading the plugin attaches the controller that **arms producers' `ctx.jobs.start()`**.

**Three tools (`job_list` / `job_output` / `job_kill`) cover every kind of background work.** Background bash, PTY sends, sub-agents all go through the same set.

This solves a real model-experience problem: if every kind of background work has its own management tools, the model has to remember 9 tool names instead of 3, and frequently picks the wrong one.

And `ctx.jobs`’s design **decouples executors from sessions** (look back at the shell Definition JSDoc in §6.5) — job ids, ownership, polling, notices belong to jobs; the executor only executes.

### 14.5 todo: allowParallelInProgress is required with no default

`dsh-tool-todo` in `docs/tool-catalog.md`:

> `todo_write` is session-owned state; UIs render the latest `todo/write` event as a checklist. **`allowParallelInProgress` is required with no default**, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task.

**Required with no default** is deliberate. Because “can several in-progress tasks exist at once” is a **product-semantics choice**, and should not have an implicit default. And that choice changes **the tool description text** — the same tool, two ways of teaching the model.

This is the landing of `AGENTS.md`’s “No hardcoded tunables in plugins”:

> deployment-varying choices are **validated `Config` fields changeable from cordis.yml**; a `DEFAULT_*` constant or test hook is **not** configurability.

### 14.6 guard: loop hygiene

`packages/guard` has only two sub-packages, no unified entry package: `repeat-tool-reminder` (injects a reminder when the same tool+canonical args are hammered consecutively, **never vetoes**) and `timeout-policy` (wraps a deadline on `tools/execute`; when a tool declared `timeoutMs` but does not honor the signal, it yields a structured `TOOL_TIMEOUT`). `AGENTS.md` writes “loop-hygiene + tool-timeout plugins”. max-steps / token budget are not in this group.

---

<h2 id="ch15">Chapter 15 · subagent, 11 packages: from a child process to “one turn of someone else’s product”</h2>

`packages/subagent` is the largest group besides client and core: **11 packages · 24 466 lines**.

### 15.1 How wide the provider range is

`docs/architecture.md:102`:

> [Subagent providers](subsystems/subagent.md) vary just as widely behind one interface, **from a fresh child agent to a delegated turn in another product**.

Behind one interface, from “start a new child agent” to “**delegate to one round of conversation in another product**.” The census shows package names such as `subagent-codex` and `subagent-claude-code` — **it treats Codex and Claude Code as subagent providers**.

This is a third shape of the same idea as this series’ Open Design (treat someone else’s CLI as the engine) and DeepTutor (treat a coding CLI as a consultable knowledge base): **dsh treats them as providers of the subagent seam**, so they automatically receive the entire infrastructure of jobs management, session recording, and the tool pipeline.

### 15.2 Three-layer tool design

The tool catalog has four subagent-related packages, with a clear division of labor:

| Package | Tools | Design point |
|---|---|---|
| `tool-subagent` | `subagent` / `subagent_fork` | **The registration name is load-time config** (`toolName`), so the same package loaded multiple times yields one tool name per backend |
| `tool-subagent-control` | `send_message` `interrupt_agent` `list_agents` | Control tools **registered globally once**, covering all continuable background children |
| `tool-subagent-report` | `report` | **Per-child registration**, visible only inside the child, and passes through the global toolFilter |

The deployment note on `tool-subagent` particularly embodies “configuration is architecture”:

> The shipped compositions load this package **once per subagent backend**, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description, `run_in_background` parameter, and system-prompt policy follow **its own** `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are **not identical**: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground.

**The same package loaded twice produces two tools with different semantics** (one continuable + default background, one one-shot + default foreground). This is something only a pluginized architecture can do — in a `main()`-shaped architecture you would have to write two tool classes.

### 15.3 `ralph`: an interesting workflow tool

`dsh-tool-ralph` in `docs/tool-catalog.md`:

> A **fixed foreground workflow** starts **one fresh structured child per round**; the model selects only the **immutable objective** and an optional round cap.

Each round starts a brand-new structured child; the model can only choose the “immutable objective” and a round cap. **The model cannot change the objective, nor the flow** — another instance of “sink the deterministic skeleton into the tool” (compare the same class of design in DeepTutor Chapter 14).

---

<h2 id="ch16">Chapter 16 · skill / hooks / mcp: living with other people’s ecosystems</h2>

### 16.1 hooks: bridging Claude Code and Codex hook protocols

In `AGENTS.md`’s repository layout:

> `hooks/` Claude Code/Codex hook bridges + wire-protocol library

`packages/hooks` (3 packages · 4 933 lines) **implements other products’ hook wire protocols**. That means a hook script written for Claude Code can run inside dsh.

This is a decision with strategic meaning: **lower migration cost**. The user’s existing hook investment is not wasted. And it is made into an independent wire-protocol library, not hardcoded into the tool pipeline.

In the pipeline diagram, hooks hang on `tools/pre-execute` (“hooks · permission · sandbox”) and produce their own session events (`hook/invoked` / `hook/result`) — **hook invocation and results are also log-visible**.

### 16.2 skill: registry + local implementation + catalog/loader tools

`packages/skill` (4 packages · 6 242 lines): provider registry + local implementation + catalog/loader tools.

One line in the tool catalog for `dsh-tool-skill` is worth noticing:

> `skill` | `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, **`user/message` replacement catalogs via `agent.inject()`**

The skill catalog is updated by **replacing the user/message catalog via `agent.inject()`** — looking back at Chapter 9, `inject` does not wake the loop, so refreshing the skill catalog does not trigger an extra model call; it waits for the next real message and goes in together. **This is a clean application of the inbox’s dual semantics.**

### 16.3 mcp: one package, 3 322 lines

`packages/mcp` has only 1 package. MCP tools’ schemas are **raw JSON-Schema** (see the generator explanation in `tool-catalog.md`), so they cannot be statically analyzed and must be read after boot.

---

<div class="part-band"><span class="band-k">Part V</span>Execution world and remote surface</div>

<h2 id="ch17">Chapter 17 · One execution world: swap two providers, five capabilities move with them</h2>

### 17.1 The architecture’s strongest selling point

`docs/architecture.md:102`:

> Seams are why **one provider swap changes the whole product**. **Filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks.**

```mermaid
flowchart TB
    subgraph prim["两个原语 seam"]
        FS["ctx.fs"]:::p
        SP["ctx.subprocess"]:::p
    end
    subgraph built["建在原语之上（26 包 · 42 885 行）"]
        SH["ctx.shell → bash / pwsh"]
        TM["ctx.terminals → 持久 PTY"]
        LSP["ctx.lsp → 语言服务"]
        SEARCH["glob / grep → 打包的 ripgrep"]
        CR["ctx.codeRuntime → 代码模式"]
    end
    FS --> built
    SP --> built
    subgraph swap["换 provider = 换整个世界"]
        L["本机：node:fs + child_process"]
        E["远程：dsh-e2b（沙箱 FS/subprocess 适配器）"]
    end
    L -.任选其一.-> prim
    E -.任选其一.-> prim
    classDef p fill:#e8f0fe,stroke:#4285f4
```

### 17.2 Code evidence for the claim

The key is **the direction of dependence**: no upper-layer capability directly imports `node:child_process`. I verified this in the census — `subprocess` is injected by 8 packages.

The search tools make the point most clearly (`dsh-tool-fs-search` in `docs/tool-catalog.md`):

> glob and grep are unconditional discovery tools that **spawn the packaged ripgrep binary (`@vscode/ripgrep`) through `ctx.subprocess`** as ordinary foreground calls (never background jobs) — **no host `rg` install and no shell layer**.

**“no shell layer”**: search does not go through the shell. So search capability does not depend on “whether the machine has bash”; it works consistently in a remote sandbox, on Windows, and in a stripped container.

### 17.3 e2b is the empirical proof of this claim

`packages/e2b` (3 packages · 6 630 lines), positioned in `AGENTS.md`:

> `e2b/` E2B POC: sandbox + FS/subprocess adapters

**A POC uses 6 630 lines to prove the architectural promise: swap two providers, five capabilities move with them.**

Sizes of the execution-world groups (census data):

| Group | Packages | Lines | Role |
|---|---|---|---|
| `fs` | 7 | 13 044 | filesystem seam + policy (including a read-before-write policy plugin) |
| `shell` | 9 | 10 405 | bash/pwsh seam + 4 providers + 3 consumers |
| `sandbox` | 4 | 9 040 | sandbox seam + policy |
| `e2b` | 3 | 6 630 | remote empirical proof |
| `terminal` | 3 | 5 721 | persistent PTY |
| `lsp` | 3 | 5 496 | language services |
| `subprocess` | 2 | 4 256 | process trees |
| `code-runtime` | 2 | 3 963 | code-mode runtime |
| **Total** | **33** | **58 555** | **all built on two primitive seams** |

### 17.4 code-runtime: Code Mode

The `run_code` entry in `docs/tool-catalog.md` is the longest deployment note in the whole lecture; it is worth understanding in full:

> Owned by the tool registry as a **reserved transport outside filterable capability layers** under `mode: code` / `mode: both`. Under `code` it is **the registry's only wire contribution**; the other visible capabilities are declared in a **generated SDK section in the loaded runtime's language**, and a program calls them through bindings scheduled under the **native concurrency contract** (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that **re-enter the complete guarded tool pipeline** and link each nested execution to this outer result.

Unpacked:

1. **Under Code Mode, `run_code` is the registry’s only wire contribution** — the model sees only one tool
2. Other capabilities appear as a **generated SDK section** (in the runtime’s language, e.g. Python/JS)
3. The program calls them through bindings, **re-entering the complete guarded tool pipeline**
4. Each nested execution **is linked to the outer result**
5. Concurrency has a native contract: submission-ordered starts; concurrency-safe bodies overlap up to `maxParallelSubCalls`

**This is a morphological jump from “tool call” to “write a program that calls tools”**, and it does not bypass any safety check — nested calls re-walk the guard pipeline. In this series Claude Code’s code execution and Reasonix’s use_capability have explored similar ground; dsh’s version is the most thorough on “nested execution is also constrained by the complete pipeline.”

---

<h2 id="ch18">Chapter 18 · Sandbox: four-platform `confine(argv)` and “isolation strength is queryable”</h2>

### 18.1 The sandbox does not execute; it only wraps argv

The extension-point table in `docs/architecture.md`:

> Confine spawned processes | use a `ctx.sandbox` backend; **consumers wrap argv before spawning**

This design lets the sandbox compose with any provider: host bash, remote subprocess, LSP child process — anyone can wrap a layer before spawn. **If the sandbox itself were responsible for execution, it would fight the executor for the same duty.**

### 18.2 The platform chain is not “Landlock only”

`PLATFORM_CHAINS` of `sandbox-local` (`packages/sandbox/sandbox-local/src/index.ts:159-165`):

| Platform | Mechanism |
|---|---|
| Linux | **bwrap first**, Landlock on failure |
| darwin | **Seatbelt / `sandbox-exec`**, the only candidate, no probe |
| win32 | **ACL restricted-token** (`dsh-sandbox-windows-acl`), enforcement reports `partial` |

The Landlock launcher is in `native/landlock-run`: the body is **C11** `main.c` (self-restrict-then-exec, the rule set inherited through `execve`; on failure **exit 125, do not exec** the target). The npm package name carries `node-addon`, but it is **not** an N-API addon; it is a prebuilt static binary + a TS wrapper (`grantArgs` → `--ro`/`--rw`). macOS/Windows have **no** Landlock port.

`pnpm-workspace.yaml`:

> The Landlock launcher is developed with its harness consumers but keeps its native build and publication scripts under `native/landlock-run`.

### 18.3 Two independent service keys

In the census, `sandbox` and `sandboxPolicy` are **two** service keys. **“Capability” and “policy” are separated** — so policy can be swapped per deployment without swapping the sandbox implementation.

On the tool side you can see traces of policy (`packages/shell/tool-bash/src/index.ts:23-25`):

```ts
import { ESCALATION_TARGETS, approveEscalation, canonicalPath, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
```

`ESCALATION_TARGETS` / `approveEscalation` / `validateEscalationArgs` — **there is a controlled privilege-escalation path**, not “all forbidden or all open.”

### 18.4 A postmortem every sandbox author should read

`docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md`

The title is the entire lesson: **Landlock’s “partial enforcement” notice once misclassified a child process’s own failures**.

When the sandbox can only partially enforce (an old kernel does not support some rule), if the “partial enforcement” hint you emit also counts **the child process’s own failure**, the user will think it is a sandbox problem and turn the sandbox off — **a classification error in a diagnostic message causes the user to voluntarily lower the security level**.

dsh has only 4 postmortems in total, and one of them is given to sandbox misclassification. That choice itself shows what they think is worth writing down.

---

<h2 id="ch19">Chapter 19 · typert: type-graph-driven RPC, and the ambition of 11 packages</h2>

### 19.1 What it is

`AGENTS.md`:

> `typert/` type graph generator, loader, and runtime registry
> `api/` Remote BFF assembly and **Typert RPC gateway**

`packages/typert` has **11 packages · 15 404 lines** — larger than goal, plan, todo, schedule, jobs, and guard combined. This is not a helper tool; this is a piece of infrastructure.

### 19.2 What problem it solves

The Web frontend (`client` 39 packages · 138 163 lines) needs to call backend services. The traditional approach: hand-write REST/RPC interfaces + hand-write type definitions + keep them in sync by hand.

typert’s approach: **generate a type graph from TypeScript types; the loader and runtime registry then provide end-to-end type-safe RPC**.

Companion docs: `docs/api-gateway.md` and `docs/subsystems/typert.md`. The census shows `dsh-api-remotes` depended on by 23 packages — frontend modules access the backend through it.

### 19.3 Why this counts as “ambition”

Because it means dsh does not intend to be only a CLI. `api/`’s positioning is “**Remote BFF** assembly” (Backend For Frontend) — this is an architecture aimed at multiple clients and multiple deployment shapes.

There is a related sentence in `AGENTS.md`’s compiler-face rules:

> **Keep compiler faces explicit.** Each package uses one aggregate **except `api/remotes`**; repo-wide programs seed a face config, never the root solution.

`api/remotes` is the only package in the warehouse allowed to have multiple compiler aggregates — because it has to face both the host face and the client face. **The exception is recorded explicitly, not silently present.**

---

<h2 id="ch20">Chapter 20 · Four fronts, one behavior: CLI / Web / ACP / SDK</h2>

### 20.1 Four shells

| Frontend | Packages | Positioning |
|---|---|---|
| **CLI** | `apps/cli` (owns the `dsh` bin) | interactive terminal + `--profile headless` one-shot tasks |
| **Web** | `apps/web` + `client` 39 packages (138 163 lines) | browser app, default `127.0.0.1:3080` |
| **ACP** | `packages/acp` (1 669 lines) | **automation-only** Agent Client Protocol server |
| **SDK** | `packages/sdk` (3 packages) + `python/sdk` | JSON-RPC protocol + server + TS client + Python SDK |

`AGENTS.md` specifically marks ACP as **"automation-only"** — it is not an interactive UI for humans; it is an automation interface for programs.

### 20.2 Web Client extension points

One row in the extension-point table of `docs/architecture.md`:

> Add a Web Client Chat node | register a `ConversationNodeDefinition` + keyed renderer

**The frontend is pluginized too.** Every kind of node in a conversation (message, tool card, diff, terminal output) is a `ConversationNodeDefinition` + a corresponding keyed renderer.

Layering of the client group’s 39 packages (in-degree in the census): `dsh-client-ui-slots` 35 times, `dsh-client-runtime` 34, `dsh-client-ui-primitives` 29, `dsh-client-locale` 27, `dsh-client-connection` 19. **UI slots are the frontend’s largest confluence point**, corresponding to the backend’s `ctx.tools`.

### 20.3 How one behavior is guaranteed

By a **snapshot-test matrix** (`docs/testing.md`):

- ACP scenarios: `examples/<name>/tests/snapshots/`, boot a real automation-server example, replay a recorded session, diff normalized JSON-RPC **plus** the re-persisted log
- headless backend scenarios: `examples/headless-agent` owns internal standard-event JSONL snapshots
- CLI interactive journeys: `apps/cli/tests/snapshots/`, JSONL-driven
- Web browser journeys: `apps/web/tests/snapshots/`, Chromium replay (**a required Linux PR gate**)

And there is a very careful rule:

> One ACP scenario (`text-turn`) **pins full system-prompt/tool-schema content**; other fixtures **tokenize it** so an edit churns one line.

**Only one scenario pins the full system-prompt and tool-schema content; other scenarios tokenize it** — so changing one prompt sentence only turns one snapshot into one changed line, not dozens of snapshots all red.

This is the key trick for scaling snapshot tests; it is worth copying on its own.

---

<div class="part-band"><span class="band-k">Part VI</span>Full roster of 241 packages and the collaboration graph</div>

<h2 id="ch21">Chapter 21 · How to read the roster, and the six functional domains</h2>

### 21.1 How this roster was produced

I wrote a census script (`scripts/dsh-plugin-census.py`) that extracts, for each of the 241 packages:

| Field | Extraction method |
|---|---|
| Package name | `name` in `package.json` |
| Role | inferred from package-name prefix + whether a `ctx` key is declared + whether a tool is registered |
| Owned `ctx` keys | `declare module '@deepseek-ai/cordis' { interface Context { … } }` in source |
| Injected services | `export const inject = [...]` and `static inject = [...]` |
| Model-visible tools | `defineTool({ name: '…' })` |
| Lines | all `.ts`/`.tsx` under the package (excluding `.d.ts`) |
| **What it does** | **the first paragraph of the source-top `@module` JSDoc (translated)** |

The last column is the most valuable part of this roster: **it is not my summary; it is the one-sentence duty the package author wrote**. 227 of 241 packages have such a JSDoc (the missing 14 are fixtures, platform binary packages, and vendored Cordis plugins).

This also side-proves the effect of the `verify-export-jsdoc` gate — **this repository’s module-level documentation coverage is high enough to be used as a roster directly**.

### 21.2 Role distribution

| Role | Count | Note |
|---|---|---|
| Provider/plugin | 97 | implements some seam, or a pure policy/glue plugin |
| **Service Definition** | **60** | owns at least one `ctx` key |
| Web client module | 39 | `client` group |
| **Consumer (tool)** | **24** | registers a model-visible tool |
| Support library | 13 | `util` / `test-support` |
| Bundle | 3 | distribution layer |
| Other | 5 | fixtures etc. |

**60 Service Definitions own 73 service keys** (a few packages own multiple keys, e.g. `agent-loop` owns both `agentLoop` and `configuredAgentIdentities`).

### 21.3 Six functional domains

52 package groups rearranged by function into six domains; the next three chapters expand domain by domain:

```mermaid
flowchart TB
    subgraph D1["域一 · 主轴（30 包 · 95 200 行）"]
        core["core 8"]; llm["llm 5"]; sess["session 13"]; sq["session-query 4"]
    end
    subgraph D2["域二 · 上下文经济（11 包 · 19 828 行）"]
        ctxg["context 4"]; comp["compaction 4"]; spill["spill 3"]
    end
    subgraph D3["域三 · 执行世界（40 包 · 64 743 行）"]
        fs["fs 7"]; sh["shell 9"]; sub["subprocess 2"]; term["terminal 3"]
        cr["code-runtime 2"]; lsp["lsp 3"]; sb["sandbox 4"]; e2b["e2b 3"]; web["web 6"]; mcp["mcp 1"]
    end
    subgraph D4["域四 · 任务治理（39 包 · 65 646 行）"]
        skill["skill 4"]; sa["subagent 11"]; wf["workflow 4"]; goal["goal 4"]
        plan["plan 1"]; todo["todo 1"]; sch["schedule 1"]; jobs["jobs 3"]
        guard["guard 2"]; inter["interaction 5"]; hooks["hooks 3"]
    end
    subgraph D5["域五 · 装配与宿主（51 包 · 71 000 行）"]
        preset["preset 2"]; bundle["bundle 3"]; boot["boot 2"]; host["host 8"]
        api["api 2"]; typert["typert 11"]; sdk["sdk 3"]; acp["acp 1"]
        misc["settings/credentials/identity/storage/<br/>workspace/attachment/feedback/<br/>runtime-diagnostics/extensions 19"]
    end
    subgraph D6["域六 · 前端与支撑（70 包 · 187 500 行）"]
        client["client 39"]; apps["apps 2"]; ex["examples 3"]
        ts["test-support 6"]; util["util 7"]; vendor["vendor 9"]; native["native 4"]
    end
    D2 & D3 & D4 -.注入/被注入.-> D1
    D5 -.装配.-> D1
    D6 -.消费.-> D5
```

---

<h2 id="ch22">Chapter 22 · Domain I · Spine (30 packages) and Domain II · Context economics (11 packages)</h2>

### 22.1 Domain I guide: five keys hold up the whole product

The 30 spine packages provide only seven core service keys: `sessions` `systemPrompt` `tools` `agents` `agentLoop` `llm` + `scope` (a library, no key). But they are the mount surface for every other plugin:

```mermaid
flowchart LR
    AL["agentLoop<br/>驱动"] -->|注入| A["agents"]
    AL -->|注入| S["sessions"]
    AL -->|注入| SP["systemPrompt"]
    AL -->|注入| T["tools"]
    AL -->|注入| L["llm"]
    T -->|注入| SP
    S -.session/event.-> P["session 组 12 包<br/>持久化/投影/标题/遥测"]
    S -.日志.-> SQ["session-query 4 包<br/>5 个只读检索工具"]
    L -.adapter seam.-> PR["llm providers"]
```

**`agent-loop` injects five services** (`agents` `llm` `sessions` `systemPrompt` `tools`, census data) — those five are the complete dependency of “run one round of conversation.” And it itself is mounted by configuration, replaceable.

Three observations on Domain I:

1. **`core/tools` at 13 743 lines is the second-largest single package** — tool registry + five-stage pipeline + presentation modes; complexity concentrates here
2. **The `session` group of 13 packages** splits “the log” into core + persistence + projection + title + telemetry + reference resolution, because their evolution cadences differ
3. **`session-query` 4 packages, 12 283 lines** provide 5 read-only tools so the model can retrieve its own history — **the agent can query its own session log**, a capability rarely seen elsewhere

#### core · product-API spine (8 packages · 40,854 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `tools` | **Def** | Tool registry, model presentation modes, and pre/guard/around/post/result execution pipeline. | owns `tools` · injects `systemPrompt` | 13,743 |
| `agent-loop` | **Def** | Concrete agent-loop plugin: creates scoped ReactLoopAgents, publishes them through the agent/session registries, and owns their ordered teardown. | owns `agentLoop`, `configuredAgentIdentities` · injects `agents`, `llm`, `sessions`, `systemPrompt`, `tools` | 12,207 |
| `session` | **Def** | Event-sourced session service: append-only session log, in-memory store, and the derived LLM message history. Persistence is a plugin concern (subscribe to `session/event`, drain on `session/flush`). | owns `sessions` | 8,401 |
| `agent` | **Def** | Agent service: live registry, factory delegation, and process-local initiator scope. Concrete creation and driving belong to the loop. | owns `agent`, `agents` | 3,206 |
| `system-prompt` | **Def** | Registry for ordered system sections, dynamic context, tool schemas, and prompt variables. | owns `systemPrompt` | 1,582 |
| `scope` | Prov | Scoped-context primitive: mint a Cordis context that tags registrations with an opaque identity and build routing-only event carriers for that identity. | — | 1,215 |
| `agent-default-model` | **Def** | Default model selection for an Agent without a session-specific selection. | owns `agentDefaultModel` | 264 |
| `agent-tool-presentation` | Prov | Agent-plane presentation selector: the row an agent preset carries to say which form of its tools the model sees. The tool registry itself stays on the host plane — the agent loop's scheduler, the API proxy's presenters, and every tool plugin are all its consumers, so it cannot move into a preset. … | injects `tools` | 236 |

#### llm · model-access seam (5 packages · 20,502 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `llm-pi-ai` | Prov | Generic pi-ai-backed LLM adapter plugin. One plugin instance owns a dict of provider routes; a route naming an installed pi-ai provider inherits that provider's endpoint, protocol, and model catalog as defaults, and a route pi-ai does not ship is declared outright. … | injects `llm` | 6,972 |
| `llm` | **Def** | LLM service: adapter registry with a waterfall-interceptable streaming call API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for provider backends, and `BlockAssembler` for chunk assembly. | owns `llm` | 5,015 |
| `llm-deepseek` | Prov | Register a {@link DeepSeekAdapter} for the `deepseek-official` provider route on `ctx.llm`, with connection facts resolved per request instead of frozen at load: the plugin layers its `cordis.yml` entry config under the optional `llm-deepseek` user-settings section (`ctx.settings`) and resolves the  … | injects `llm` | 3,705 |
| `token-meter` | **Def** | Single replay-aware token-meter service for request and surface pressure. | owns `tokenMeter` | 2,477 |
| `llm-retry` | Prov | Provider-routed model-request retry policy on the agent loop's request recovery extension point. Each scheduled retry is durable before its cancellable wait. | injects `agents` | 2,333 |

#### session · durable session log (13 packages · 21,520 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `session-persistence` | **Def** | Durable session-persistence Service Definition (`ctx.sessionPersistence`). Backends store {@link SessionEvent}s as the event-sourced log and carry non-replayable {@link SessionHeader} metadata separately. | owns `sessionPersistence` | 6,657 |
| `session-persistence-jsonl` | Prov | JSONL durable session-persistence backend. It stores a header and contiguous events in one append-only file per session, and delegates orchestration to {@link PersistenceCoordinator}. Its side-effect-free locator returns the absolute per-session log target before materialization. | injects `sessions` | 4,570 |
| `session-title` | **Def** | Log-backed session title service, deterministic fallback, and provider contract. | owns `sessionTitle` · injects `sessions` | 2,339 |
| `session-persistence-sqlite` | Prov | SQLite durable session-persistence backend. It maps each session header and event to rows, and delegates write-path orchestration to {@link PersistenceCoordinator}. It has no independent per-session artifact, so its locator returns `undefined`. | injects `sessions` | 1,667 |
| `session-telemetry` | **Def** | SessionTelemetryBackend Service Definition for the DeepSeek Harness. This package owns the CAPTURE side of session-event reporting — which records exist (the chunk projection), what they carry (the logical record), when they are captured (adoption, the per-append firehose, lifecycle forwarding), liv … | owns `sessionTelemetry` | 1,215 |
| `session-telemetry-otel` | Prov | OpenTelemetry Service Provider for the DeepSeek Harness telemetry capability. Composes the OTel JS SDK as-is — a `LoggerProvider` with a `BatchLogRecordProcessor` and an OTLP/HTTP log exporter — and maps each record handed over by the capture coordinator onto `logger.emit()`. … | injects `sessions` | 1,004 |
| `session-projection` | **Def** | Service Definition and drive registry for the session-projection capability seam: the merge-extensible `SessionProjectionMap` type table, the `ProjectionDefinition` state-driven computation unit contract, and the `ctx.sessionProjections` registry that DRIVES every registered unit forward eagerly ove … | owns `sessionProjections` | 852 |
| `session-projection-cache` | **Def** | Persisted projection cache (`ctx.sessionProjectionCache`): durable checkpoints of every registered projection unit's state, one record per session on the domain data form (`session_projcache` domain — the shipped json backend lands it beside `workspace.json`). … | owns `sessionProjectionCache` · injects `sessionPersistence`, `sessionProjections`, `sessions`, `storageDomain` | 796 |
| `session-title-llm` | Prov | Shared route, framing, timeout, assembly, and validation policy for model-backed session-title providers. | — | 692 |
| `session-stats` | Prov | Function plugin registering the `sessionStats` projection unit: whole-log turn/step counts and LLM/tool/first-token/decode wall times served through the session-projection seam (registry snapshot, change feed, and every projection carrier), so clients render full-session figures that paging and comp … | injects `sessionProjections` | 688 |
| `session-checkpoint-policy` | Prov | Semantic durability checkpoints for model requests, top-level tool dispatch, and completed agent steps. | injects `llm`, `sessionPersistence`, `sessions`, `tools` | 559 |
| `session-title-first-prompt-llm` | Prov | First-human-message model provider for `ctx.sessionTitle`. | injects `llm`, `sessionTitle`, `sessions` | 339 |
| `session-title-all-prompts-llm` | Prov | All-human-messages model provider for `ctx.sessionTitle`. | injects `llm`, `sessionTitle`, `sessions` | 142 |

#### session-query · log retrieval (4 packages · 12,323 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `session-query-sqlite` | **Def** | Concrete session-query service with SQLite FTS5 over the live-preferred corpus. | owns `launcherSessionQueryPath` | 3,936 |
| `session-query` | **Def** | Service Definition for combined session-history reads, traces, filters, and full-text search. | owns `sessionQuery` · injects `sessions` | 3,770 |
| `tool-session-query` | **Tool** | Model-facing, workspace-authorized session-history search and read tools. | injects `sessionQuery`, `systemPrompt`, `tools` · tools `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | 3,758 |
| `session-log-export` | Prov | Web Session-log download command over the host endpoint owned by ApiProxy. | injects `commands` | 859 |

### 22.2 Domain II guide: four keys manage context

See Chapter 13. This domain is only 11 packages and 19 828 lines, but it decides whether a long session can survive.

The `context` group (4 packages · 10 251 lines) is “request-context plugins” — they decide what enters a request, in what order. The division of labor with `systemPrompt` is: `systemPrompt` owns **system-prompt sections**, `context` owns **runtime-context projections** (look back at `runtimeContext.project()` in Chapter 8).

#### context · request-context assembly (4 packages · 10,280 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `agent-instructions` | Prov | Workspace instruction loader for AGENTS.md-compatible files. Baseline instructions enter durable context before the first request; successful fs tool touches project nested, changed, and removed instructions into the inbox. … | — | 6,643 |
| `time-context` | Prov | Opt-in request clock context. Eligible steps add durable, source-attributed time readings to the request history. | injects `agents` | 1,549 |
| `session-reference` | **Def** | Cross-session snapshot preparation. Hosts adapt mentions into structured references; this service owns exact reads, projection, budgets, and durable context. | owns `sessionReferenceResolver` · injects `sessionQuery` | 1,395 |
| `tmux-context` | Prov | Opt-in request-preparation tmux-location context. Eligible step attempts append durable, source-attributed context naming the tmux session, window, and pane this agent process runs in, plus the window's pane-tree layout. … | injects `agents` | 693 |

#### compaction · context compaction (4 packages · 8,063 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `compaction-basic` | Prov | Basic replay-aware compaction backend. | injects `llm`, `sessions`, `tokenMeter` | 4,958 |
| `compaction` | **Def** | Compaction Service Definition (`ctx.compaction`): providers decide when to compact and replace a history range with one summary node by subclassing {@link CompactionEngine}. … | owns `compaction` | 1,800 |
| `compaction-tool-result-pruner` | **Def** | Replay-safe, model-free tool-result pruning service. | owns `toolResultPruner` · injects `tokenMeter` | 681 |
| `command-compact` | Prov | Human-facing `/compact` command over the backend-independent compaction seam. | injects `commands`, `compaction` | 624 |

#### spill · oversized-output spill (3 packages · 1,485 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `spill-policy` | Prov | The spill-policy PLUGIN: a `tools/post-execute` result transformer that keeps oversized plain-text tool results out of the model's context. When a final result's UTF-8 size exceeds `maxInlineBytes`, it saves the FULL text to a session-scoped spill artifact (`ctx.spillStore`) and replaces the model-f … | injects `tools` | 896 |
| `spill-local` | Prov | `LocalSpillStore`: the host-filesystem implementation of the `@deepseek-ai/dsh-spill` storage seam. Persists a tool's oversized text to a private, session-scoped file (see `./store.ts` for the traversal-safe naming and exclusive owner-only write) and returns a path locator plus local read/grep retri … | — | 364 |
| `spill` | **Def** | Service Definition for the spill storage capability seam (`ctx.spillStore`): an abstract service defining WHAT a spill backend does — persist a tool's oversized text and return a model-facing locator plus retrieval guidance — without saying HOW. … | owns `spillStore` | 225 |

<h2 id="ch23">Chapter 23 · Domain III · Execution world (40 packages) and Domain IV · Task governance (39 packages)</h2>

### 23.1 Domain III guide: two primitives + eight capabilities

See Chapter 17. The most important way to read this domain is **the direction of dependence**: `fs` and `subprocess` are primitives; `shell` / `terminal` / `lsp` / `code-runtime` / search tools are built on them; `sandbox` is cross-cutting (wraps argv); `e2b` is the remote empirical proof.

The `web` group (6 packages · 5 985 lines) is another independent seam: behind `ctx.web` are swappable search/fetch providers; the model sees only two stable tools, `web_search` / `web_fetch`.

#### fs · filesystem seam and policy (7 packages · 13,088 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `tool-fs` | **Tool** | Model-facing read, read_image, write, and edit tools over `ctx.fs`. This package owns schemas, validation, read windows, formatting, and observation events, never a concrete provider. An optional event policy supplies mutation guards; without one the tools use unconditional provider calls. | injects `fs`, `systemPrompt`, `tools` · tools `edit`, `read`, `read_image`, `write` | 3,905 |
| `tool-fs-search` | **Tool** | The model-facing filesystem discovery tool suite (`glob`, `grep`) over the packaged ripgrep binary (`@vscode/ripgrep`). This single plugin registers both tools; the binary ships inside the npm dependency, so no system `rg` install and no shell layer is involved. … | injects `subprocess`, `systemPrompt`, `tools` · tools `glob`, `grep` | 3,247 |
| `fs-local` | Prov | Host-filesystem implementation of `ctx.fs`. Realpath-derived target identity makes aliases share stale guards, and writes through a symlink update its target without replacing the link. | — | 3,073 |
| `tool-str-replace-editor` | **Tool** | Model-facing `str_replace_editor` over the Harness filesystem seam. | injects `fs`, `tools` · tool `str_replace_editor` | 1,135 |
| `fs` | **Def** | Filesystem Service Definition for one execution world. Backends own stable target identity, process paths and file URIs, containment, text reads, decoding, binary rejection, and atomic mutations. … | owns `fs` | 747 |
| `fs-sandbox` | Prov | `SandboxedFileSystem`: the sandbox-enforcing implementation of the `@deepseek-ai/dsh-fs` Service Definition. It extends `LocalFileSystem` so all text-storage mechanics — resolve, stat, read/stream, list, the atomic write and the read-match-write edit critical section — are the local implementation's … | injects `sandboxPolicy` | 552 |
| `fs-observation-policy` | Prov | Event-only filesystem observation policy; it registers no service. A weak owner/target map records every authoritative presence/absence observation, single-slot intent listeners derive guards from that state, and the provider performs the atomic freshness/no-clobber check. … | — | 429 |

#### shell · bash/pwsh execution seam (9 packages · 10,453 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `tool-bash` | **Tool** | Model-facing Consumer of the `ctx.shell` capability seam. Background calls register process handles with `ctx.jobs`; their work uses job cancellation rather than the tool-call signal after an id is returned. … | injects `shell`, `shellEnv`, `systemPrompt`, `tools` · tool `bash` | 2,081 |
| `tool-pwsh` | **Tool** | Model-facing PowerShell Consumer of the `ctx.shell` capability seam. Intended for Windows compositions where a PowerShell executor (e.g. `@deepseek-ai/dsh-pwsh-local`) backs `ctx.shell`; the tool contract is PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables. … | injects `shell`, `shellEnv`, `systemPrompt`, `tools` · tool `pwsh` | 1,900 |
| `bash-sandbox` | Prov | Sandbox-consuming bash executor. It wraps the exact local bash argv through `ctx.sandbox`, inherits local process mechanics, and reports the selected mode, enforcement, and denial facts. … | — | 1,594 |
| `tool-bash-persistent` | **Tool** | Model-facing persistent `bash` tool over the owner-scoped PTY seam. | injects `terminals`, `tools` · tool `bash` | 1,213 |
| `pwsh-local` | Prov | Local PowerShell Service Provider for the bash capability seam. Each command runs as `pwsh -NoLogo -NoProfile -NonInteractive -Command <command>` in a managed process spawned through `ctx.subprocess`; the executor owns command defaulting, deadlines and cause classification, the model-friendly termin … | injects `subprocess` | 1,083 |
| `bash-local` | Prov | Local Service Provider for the bash capability seam over the subprocess capability seam. Public commands run as `bash -c` in a managed process group spawned through `ctx.subprocess`; subclasses may reuse the same mechanics with an explicit argv. … | injects `subprocess` | 831 |
| `pwsh-sandbox` | Prov | Sandbox-consuming PowerShell executor — the pwsh twin of `@deepseek-ai/dsh-bash-sandbox`. It wraps the exact local pwsh argv through `ctx.sandbox` (which on Windows resolves to the ACL restricted-token runner chain), inherits local process mechanics, and reports the selected mode, enforcement, and d … | — | 787 |
| `shell-env` | **Def** | Tool-independent shell environment plugin: owns the `ctx.shellEnv` registry of trusted, per-execution `DSH_*` variables consumed by the model-facing shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`). … | owns `shellEnv` | 488 |
| `shell` | **Def** | Service Definition for the `ctx.shell` capability seam, covering foreground commands and background process handles. Job ids, ownership, polling, and notices belong to `@deepseek-ai/dsh-jobs`, keeping executors independent of sessions. | owns `shell` | 476 |

#### subprocess · process trees (2 packages · 4,272 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `subprocess-local` | Prov | Local Service Provider for the subprocess capability seam. Each spawn is a detached process tree with the spec's per-stream stdio dispositions. Normal disposal terminates and joins live trees; Node's synchronous exit phase force-stops any trees the service still owns. … | — | 3,740 |
| `subprocess` | **Def** | Service Definition for the subprocess capability seam (`ctx.subprocess`): execution-world executable lookup, fully specified managed process trees with raw or collected stdio, and one terminal-process primitive. … | owns `subprocess` | 532 |

#### terminal · persistent PTY (3 packages · 5,741 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `terminal-bash` | Prov | Persistent shell PTY backend over the subprocess terminal primitive, shared sandbox policy, bounded output, and provider-owned session cleanup. | injects `sandboxPolicy`, `subprocess`, `terminals` | 3,141 |
| `tool-terminal` | **Tool** | Six model-facing persistent terminal tools. Owner identity comes from the exact tool execution Agent; generic `ctx.jobs` owns background ids and collection. | injects `systemPrompt`, `terminals`, `tools` · tools `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | 1,310 |
| `terminal` | **Def** | Owner-scoped persistent PTY registry. Backends own terminal mechanics while this service owns ids, publication, authorization, and awaited cleanup. | owns `terminals` | 1,290 |

#### code-runtime · code-mode runtime (2 packages · 3,982 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `code-runtime-worker-thread` | Prov | Worker-thread code runtime: a fresh worker runs each host-type-stripped TypeScript program and bridges bindings over its message port. This is containment, not a security boundary: model code has bash-equivalent trust despite an empty environment, a heap cap, measured event-loop busy-time and wall-t … | — | 3,538 |
| `code-runtime` | **Def** | Service Definition for the code-execution capability seam that runs one model-written program against host async bindings. Runtimes know nothing about tools or sessions; consumers own those concerns. | owns `codeRuntime` | 444 |

#### lsp · language services (3 packages · 5,528 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `lsp-stdio` | Prov | Generic stdio language-server backend for `ctx.lsp`. One plugin instance configures a named table of server commands and registers one isolated provider for each entry. … | injects `fs`, `lsp`, `subprocess` | 3,972 |
| `tool-lsp` | **Tool** | Model-facing `lsp` tool over `ctx.lsp`. One read-only tool with four operations (`goToDefinition`/`findReferences`/`goToImplementation`/`hover`); it converts one-based UTF-16 cursor coordinates to the seam's zero-based positions, requires the session workspace with no fallback, caps and renders resu … | injects `lsp`, `systemPrompt`, `tools` · tool `lsp` | 1,025 |
| `lsp` | **Def** | Service Definition for the LSP capability seam (`ctx.lsp`): a language-server provider registry and per-query, order-independent selection over normalized goToDefinition/findReferences/goToImplementation/ hover queries. … | owns `lsp` | 531 |

#### sandbox · sandbox and policy (4 packages · 9,089 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `sandbox-windows-acl` | Prov | Windows ACL write-restriction sandbox backend for the DeepSeek Harness sandbox seam. Mirrors the mechanism of github.com/huoyaoyuan/ windows-acl-restrict-poc @ 10e4dfb (the fixed revision): a WRITE_RESTRICTED token whose restricting SIDs include distinct workspace and temp write SIDs that this sandb … | — | 5,889 |
| `sandbox-local` | Prov | Local sandbox backend. It selects the platform runner chain (Linux bwrap then Landlock; macOS Seatbelt; Windows the ACL restricted-token runner), functionally probes competing candidates once, and reports each wrap's enforcement and stderr classification facts. … | — | 1,975 |
| `sandbox` | **Def** | Service Definition for the same-world process-confinement capability seam: wrap exact subprocess argv under a host-path file policy. Containers, microVMs, and remote execution replace the surrounding capability seam instead; this service shares the host kernel and filesystem. | owns `sandbox` | 644 |
| `sandbox-policy` | **Def** | The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the deployment's sandbox fallbacks plus per-session resolution: the file-effect {@link SandboxMode}, the `workspace-write` root, and the override kit (the `sandbox/mode` event, its fold, and its write path, from `./session-mode.ts`). … | owns `sandboxPolicy` | 581 |

#### e2b · remote-sandbox POC (3 packages · 6,646 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `subprocess-e2b` | Prov | E2B Service Provider for the subprocess capability seam. Each handle starts through the shared sandbox and retains command output/status paths in that remote world. | injects `e2b` | 4,578 |
| `fs-e2b` | Prov | E2B provider for the filesystem capability seam. Paths, contents, and atomic staging files remain inside the shared remote sandbox. | injects `e2b` | 1,421 |
| `e2b` | **Def** | Shared ownership of one E2B sandbox. Capability adapters await the same SDK handle, so filesystem and process operations inhabit one remote Linux world. | owns `e2b` | 647 |

#### web · search and fetch (6 packages · 6,010 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `tool-web` | **Tool** | Model-facing `web_search` and `web_fetch` tools over `ctx.web`. This package owns schemas, validation, prompt guidance, limits, and presentation, never concrete providers. … | injects `systemPrompt`, `tools`, `web` · tools `web_fetch`, `web_search` | 1,960 |
| `web-search-deepseek` | Prov | Register a DeepSeek-backed provider in `ctx.web`. It calls the Anthropic-compatible Messages API with native `web_search_20250305`. The provider reuses `DEEPSEEK_API_KEY` but not `DEEPSEEK_BASE_URL`, because search and chat-completions use different bases. | injects `web` | 1,399 |
| `web-fetch-http` | Prov | `@deepseek-ai/dsh-web-fetch-http`: registers an anonymous public HTTP(S) `WebFetchProvider` with `ctx.web`. A function/namespace plugin (NOT a default-export service): it registers INTO the seam's fetch registry, like the search providers register into the search registry. | injects `web` | 910 |
| `web-search-exa` | Prov | `@deepseek-ai/dsh-web-search-exa`: registers an Exa-backed `WebSearchProvider` with `ctx.web`. A function/namespace plugin (NOT a default-export service): a search provider does not own the `ctx.web` key — it registers INTO the seam's provider registry, exactly as `@deepseek-ai/dsh-llm-deepseek` reg … | injects `web` | 593 |
| `web` | **Def** | Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and fetch. Duplicate ids are rejected. … | owns `web` | 580 |
| `web-search-perplexity` | Prov | `@deepseek-ai/dsh-web-search-perplexity`: registers a Perplexity-backed `WebSearchProvider` with `ctx.web`. A function/namespace plugin (NOT a default-export service): it registers INTO the seam's provider registry, like `@deepseek-ai/dsh-llm-deepseek` registers an adapter into `ctx.llm`. | injects `web` | 568 |

#### mcp · MCP bridge (1 package · 3,333 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `mcp-client` | Prov | MCP client bridge plugin: connects to an external MCP server and registers its tools on `ctx.tools` under server-qualified public names (`mcp__<serverName>__<rawName>`). Each plugin instance connects to one MCP server; load multiple instances in `cordis.yml` for multiple servers. … | injects `tools` | 3,333 |

### 23.2 Domain IV guide: the shared pattern of governance plugins

The 39 governance packages share a pattern I summarize as **“sink determinism into the tool, leave judgment to the model”**:

| Package | Deterministic part sunk into the tool | Judgment left to the model |
|---|---|---|
| `goal` | round counting, CAS check, blocked lower bound, human root authority | whether the goal is actually achieved |
| `plan` | mode state machine, step-boundary recording, reject calls outside the mode | plan content |
| `todo` | session state, parallelism policy | task decomposition |
| `schedule` | time semantics, bounded frequency, persistence barrier | when something should be timed |
| `jobs` | job ids, ownership, polling, termination | what should go to the background |
| `guard` | loop-hygiene thresholds, tool timeout | — |
| `interaction` | permission presets, one-shot approval semantics | — |
| `subagent` | lifecycle, background delivery, settlement | what to delegate to whom |
| `workflow` | workflow engine, worker threads | script content |

This division of labor is a different implementation of the same idea as this series’ MiMo Code (independent Goal referee), grok-build (Goal Mode five-piece set), Reasonix (Delivery Profile evidence sign-off), DeepTutor (mastery gates sunk into tools). **dsh’s distinctive feature is that each of these is an independent plugin; the loop knows nothing about them.**

#### skill · skills (4 packages · 6,255 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `skill-filesystem` | Prov | Local filesystem skill provider. This package is one implementation of the `ctx.skills` provider registry. It discovers directory-bundle and flat Markdown skills from project, custom, and user roots, parses YAML frontmatter, and loads bodies through `ctx.fs` when a filesystem service is present. | injects `skills` | 2,401 |
| `skill` | **Def** | Agent skill provider registry. This package owns the Service Definition role of the skill capability seam. Concrete providers such as `@deepseek-ai/dsh-skill-filesystem` decide where skills come from; this service only merges provider catalogs, resolves the winning skill for a name, and exposes the  … | owns `skills` | 2,172 |
| `tool-skill` | **Tool** | Durable session skill catalog and model-facing `skill` loader tool. | injects `agents`, `skills`, `tools` · tool `skill` | 1,549 |
| `skill-badge` | Prov | Bundled `dsh-badge` skill provider. | injects `skills` | 133 |

#### subagent · sub-agent seam (11 packages · 24,555 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `subagent` | **Def** | Service Definition for the subagent capability seam (`ctx.subagents`): a named-provider registry plus a capability-validating asynchronous start API. Providers establish a child before returning its run, so fulfillment is the single publication and ownership-transfer boundary. … | owns `subagents` | 9,703 |
| `subagent-codex` | Prov | Fixed Codex one-shot subagent provider. Every accepted run starts a fresh official `codex app-server --stdio` process in the delegating Session's workspace and publishes only after an ephemeral thread exists. | injects `subagents`, `subprocess` | 2,783 |
| `subagent-claude-code` | Prov | Fixed Claude Code one-shot subagent provider. Every accepted run invokes the official Agent SDK in the delegating Session's workspace and places the SDK-spawned real CLI under the shared subprocess owner. | injects `subagents`, `subprocess` | 2,226 |
| `tool-subagent` | **Tool** | Model-facing delegation through one configured `ctx.subagents` provider. Provider lifecycle controls tool registration and context-sensitive schema wording. Foreground calls always dispose the run after collection. … | injects `subagents`, `systemPrompt`, `tools` | 1,984 |
| `subagent-in-process-driver` | Prov | Shared driver for in-process ONE-SHOT subagent providers. The agent factory's creation transaction owns unpublished setup and rollback; after publication the returned AgentHandle is the one quiescent lifecycle owner held by the provider's caller. … | — | 1,940 |
| `subagent-acp` | Prov | Out-of-process ACP subagent backend. Each child has its own process, session, model, and tools, so it shares no Cordis context and advertises no parent-enforced start capabilities; the ONE thing it reads off `request.parent` is the session's workspace cwd (see {@link resolveCwd}). … | injects `subagents`, `subprocess` | 1,920 |
| `tool-subagent-control` | **Tool** | The globally named `send_message` and `interrupt_agent` tools: thin model-facing adapters over `ctx.subagents.followup()` and `ctx.subagents.interrupt()`. … | injects `subagents`, `tools` · tools `interrupt_agent`, `send_message` | 1,117 |
| `subagent-dsh-sdk` | Prov | Out-of-process SDK subagent backend. Each child is a complete DeepSeek Harness runtime in its own process — own `cordis.yml`-decided composition, session, model route, and tools — driven over stdio JSON-RPC through the TypeScript SDK client, so it shares no Cordis context and advertises no parent-en … | injects `subagents` | 969 |
| `tool-subagent-report` | **Tool** | The child-scoped `report` tool and its usage guidance, installed into every continuable in-process child's unpublished context. Roots, one-shot children, remote providers, and agentless executions never see the registration. | injects `subagents`, `systemPrompt`, `tools` · tool `report` | 734 |
| `subagent-spawn-in-process` | Prov | The in-process SPAWN subagent backend: registers a {@link SubagentProvider} on `ctx.subagents` that runs each child as a fresh child {@link Agent} on the same cordis context (its own session, own system prompt, zero parent context). … | injects `subagents` | 692 |
| `subagent-fork-in-process` | Prov | The in-process FORK subagent backend: registers a {@link SubagentProvider} on `ctx.subagents` that runs each child as a child {@link Agent} SEEDED with a prefix of the parent's session log — so the child inherits the parent's conversation context instead of starting fresh. … | injects `subagents` | 487 |

#### workflow · workflow engine (4 packages · 7,734 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `workflow-worker-thread` | Prov | Worker-thread workflow engine. Each run executes its model-written script in an escapable vm context on a fresh worker and bridges `agent()` calls to host subagents. … | injects `subagents` | 4,566 |
| `tool-workflow` | **Tool** | The model-facing `workflow` tool: run a JavaScript orchestration script that fans out subagents, and return the script's final value. It owns the model-facing schema and run lifecycle; script parsing, execution, caps, and cancellation live behind `ctx.workflowEngine` (`@deepseek-ai/dsh-workflow`), s … | injects `systemPrompt`, `tools`, `workflowEngine` | 1,217 |
| `tool-ralph` | **Tool** | Model-facing foreground Ralph loop over the workflow and subagent seams. A fixed script starts one fresh structured-output child per round, carrying only the immutable objective and the previous bounded handoff between them. | injects `subagents`, `systemPrompt`, `tools`, `workflowEngine` · tool `ralph` | 1,189 |
| `workflow` | **Def** | Service Definition for the workflow capability seam. Service Providers execute orchestration scripts; observe-only lifecycle events never expose run control. | owns `workflowEngine` | 762 |

#### goal · same-session goals (4 packages · 5,812 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `goal` | **Def** | Same-session goal domain: event-sourced state, compare-and-set mutations, and process-local continuation activation. | owns `goals` · injects `agents` | 2,480 |
| `goal-round-driver` | Prov | Same-session goal-round driver over public agent, session, and goal services. | injects `agents`, `goals`, `sessions` | 1,761 |
| `tool-goal` | **Tool** | Model-facing `get_goal`, `create_goal`, and `update_goal` tools over the persisted same-session goal domain. | injects `agents`, `goals`, `systemPrompt`, `tools` · tools `create_goal`, `get_goal`, `update_goal` | 1,134 |
| `command-goal` | Prov | Human-facing `/goal` command over the persisted same-session goal domain. | injects `commands`, `goals` | 437 |

#### plan · plan mode (1 package · 2,036 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `plan-mode` | **Def** | Plan mode is logged per-agent collaboration state: while active, a deployment-owned guidance section is included in each model request, and `exit_plan_mode` presents the completed plan for user review, while the `/plan off` command lets a user leave directly. … | owns `planMode` · injects `systemPrompt`, `tools` | 2,036 |

#### todo · todos (1 package · 1,012 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `tool-todo` | **Tool** | Model-facing whole-list replacement. Each call appends a `todo/write` snapshot to the calling agent's session; replay is last-write-wins, and UIs render from session events. A non-agent caller has no owning list and is rejected. Named exports preserve loader injection metadata. | injects `tools` · tool `todo_write` | 1,012 |

#### schedule · timing (1 package · 4,314 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `schedule` | **Tool** | Agent-scoped durable one-shot and fixed-rate reminders over the session event log. | injects `agents`, `sessionPersistence`, `sessions`, `tools` · tools `schedule_create`, `schedule_delete`, `schedule_list` | 4,314 |

#### jobs · background jobs (3 packages · 3,712 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `jobs-local` | Prov | Process-local provider for the background-job capability seam (`ctx.jobs`). It keeps every record in memory and hands out fresh snapshots, never live state. Registrations outlive producer and controller fibers. … | — | 1,774 |
| `tool-jobs` | **Tool** | Model-facing `job_output`, `job_list`, and `job_kill` tools over `ctx.jobs`. Loading the plugin attaches the controller required by producers. It also delivers unreported completions to the owning agent: injected into a busy owner's next step, or opening a turn on an idle one under the default `wake … | injects `jobs`, `systemPrompt`, `tools` · tools `job_kill`, `job_list`, `job_output` | 1,325 |
| `jobs` | **Def** | The background-job Service Definition (`ctx.jobs`). It owns the contract for job ids, session-scoped access, lifecycle state, completion listeners, and owner cleanup while producers retain their execution resources. The process-local registry lives in `@deepseek-ai/dsh-jobs-local`. | owns `jobs` | 613 |

#### guard · loop hygiene (2 packages · 1,027 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `repeat-tool-reminder` | Prov | Advisory per-agent repeat-call detector. It enriches post-execute decisions with logged model context without vetoing or rewriting calls. Configuration and chain semantics live in the package README; rationale lives in the repeat-tool-reminder Agent Note. | — | 669 |
| `tool-call-timeout-policy` | **Tool** | Cooperative tool-call timeout enforcer. A tool declares `timeoutMs` and promises to honor `exec.signal`; this wrapper arms that deadline and maps its own expiry to `TOOL_TIMEOUT` without racing or abandoning the tool promise. … | injects `tools` | 358 |

#### interaction · approval / permission / commands / ask-the-human (5 packages · 4,225 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `commands` | **Def** | Plugin-owned human-command registry shared by interactive UI adapters. | owns `commands` | 1,143 |
| `user-approval` | **Def** | Service Definition for the approval capability seam, covering requests, cancellation, audit, and per-session policy. Missing answerers fail closed; grants apply only to the requested action. | owns `approval` | 1,143 |
| `permission-presets` | **Def** | User-facing permission presets over the independent sandbox-mode and approval-policy knobs. A switch records the selected preset, then writes changed knobs through their canonical setters. Execution, prompt narration, and replay keep reading their knob folds. … | owns `permissionPresets` · injects `approval`, `sessions`, `shell` | 1,020 |
| `user-questions` | **Def** | Service Definition for the user-questions capability seam (`ctx.userQuestions`): a UI-backed service for pausing an agent tool call until the human answers a question. The model- facing tool lives in `@deepseek-ai/dsh-tool-ask-user`; UI packages provide the single active provider. | owns `userQuestions` | 465 |
| `tool-ask-user` | **Tool** | Model-facing Consumer of the `ctx.userQuestions` capability seam. The tool pauses until a UI provider returns a human answer, then feeds that answer back into the agent loop as an ordinary tool result. | injects `tools`, `userQuestions` · tool `ask_user_question` | 454 |

#### hooks · external hook-protocol bridges (3 packages · 4,968 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `hooks-claude-code` | Prov | Bridge for unmodified Claude Code command hooks on harness interception extension points. It supports SessionStart, prompt/tool pre/post, Stop, and subagent start/stop. … | injects `shell` | 1,837 |
| `hook-protocol` | Prov | Shared, non-plugin hook protocol library: matching, command execution and decoding, restrictive outcome merging, durable event helpers, and detached run quiescence. Claude Code and Codex bridges own their distinct payloads, environment rules, matcher mode, and typed extension-point mappings. | — | 1,702 |
| `hooks-codex` | Prov | Bridge for unmodified Codex command hooks on harness interception points. It supports five points (SessionStart, prompt/tool pre/post, Stop), regex-only matchers, snake_case payloads without a trailing newline, no hook environment or command substitution, and no pre-tool approval or rewrite path; on … | injects `shell` | 1,429 |

<h2 id="ch24">Chapter 24 · Domain V · Assembly and host (51 packages) and Domain VI · Frontends and support (70 packages)</h2>

### 24.1 Domain V guide: from YAML to a running tree

The assembly chain (drawn in tutorial step 19):

```mermaid
flowchart LR
    PJ["package.json 的 dsh 字段<br/>dsh.profile / dsh.bundle"] --> B["bundle 有序叠层"]
    B --> P1["profile 的 cordis.patch.yml"]
    P1 --> P2["home 级 cordis.patch.yml"]
    P2 --> P3["--patch CLI 覆盖层"]
    P3 --> LD["Loader：!!js 表达式插值<br/>config（注入激活后）+ disabled（每次 mount）"]
    LD --> TREE["插件树"]
    TREE --> PRESET["preset：逐会话重装配<br/>service 行需要 isolate realm"]
```

Three observations:

1. **`dsh-base` is a 117-line package with no runtime API** — its entire substance is a `cordis.patch.yml`. The JSDoc’s own words: “this module carries no runtime API.” **Configuration-is-architecture, literally.**
2. **`typert` 11 packages, 15 404 lines** is the largest investment in this domain; see Chapter 19.
3. **The `host` group, 8 packages, 22 412 lines** provides host capabilities (keys such as `dshHomePath` `launchEnvironment` `cmdlineArgs` `directoryPicker` `appExit`) — the “process/machine” layer is also made into services.

#### preset · per-session capability assembly (2 packages · 3,814 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `agent-presets` | **Def** | Agent presets: each session composes its model-facing plugin set from one preset `cordis.yml`, mounted ONCE per preset under a standing scope and joined by every agent that names it. … | owns `agentPresets` · injects `loader` | 3,570 |
| `persona` | Prov | A per-agent persona as a composable row. `dsh-system-prompt` owns the global persona as its own config, and registers that section unconditionally — so this row is **scope-only**. … | injects `systemPrompt` | 244 |

#### bundle · distribution layer (3 packages · 1,424 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `web-app` | Bundle | plus the bundle patch (`cordis.patch.yml`, declared by the `dsh.bundle.patch` manifest field). The plugin owns the browser-surface glue: it resolves the built frontend dist (workspace knowledge of this bundle, never user config), mounts the `frontend-static` fallback owner over it, registers the har … | injects `webServer` | 707 |
| `headless` | Bundle | rides over dsh-base without Host, HTTP, or browser plugins; this runner creates one Agent through the core registry, drives the task to quiescence, flushes its Session, prints the final assistant text, and exits. | injects `agentDefaultModel`, `agents`, `sessions` | 600 |
| `base` | Bundle | package's substance is `cordis.patch.yml`, declared by the `dsh.bundle.patch` manifest field and resolved by the profile composer through that field; this module carries no runtime API. | — | 117 |

#### boot · boot glue (2 packages · 4,094 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `app-boot` | **Def** | Shared boot glue for the app bins (`dsh`, `dsh-acp-demo`): load the gitignored `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to config expressions, and drive the Co … | owns `dshHomePath` | 3,657 |
| `cmdline` | **Def** | it boots. The launcher parses only its own flags (`--profile`, `--patch`, the config dumps) and hands everything after them to the tree verbatim through the {@link CmdlineArgs} service, so an app owns its flag family, its `--help` text, and its parse errors instead of the launcher knowing them. … | owns `appExit`, `cmdlineArgs` | 437 |

#### host · host capabilities (8 packages · 22,513 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `host-apiproxy` | **Def** | the ApiProxy contract (api/: types + zod schemas, browser-safe), the fetch carrier pair (fetch/: toFetchHandler on the host side, AbstractApiClient + platform subclasses on the client side), and the host-side implementation (api-proxy.ts: createApiProxy + the ApiProxyService gateway plugin providing … | owns `apiProxy` · injects `agentDefaultModel`, `agents`, `attachments`, `directoryPicker`, `llm` +6 | 18,430 |
| `host-directory-picker-native` | Prov | Native backend of the directory-picker seam: registers `ctx.directoryPicker` with the `native` capability, opening one native OS chooser on the host display per pick (macOS `osascript`, Linux Zenity with a KDialog fallback; Windows opens the modern `IFileOpenDialog` in a spawned child process — a ko … | — | 1,634 |
| `host-directory-picker-browse` | Prov | Browse backend of the directory-picker seam: registers `ctx.directoryPicker` with the `browse` capability — one-level directory listing and child-directory creation over the host filesystem via Node's stdlib (which already carries the per-OS adaptation). … | — | 599 |
| `host-directory-picker-auto` | Prov | Adaptive chooser of the directory-picker seam: resolves the host's situation once at boot (bind host, SSH launch, display session, Linux chooser binary) and mounts the matching interaction — `native` or `browse` — as real Loader entries in the in-memory root tree. … | injects `loader`, `webServer` | 580 |
| `host-webserver` | **Def** | server plus the `webServer` service (HTTP and upgrade route registries, index transform taps, and the single fallback seat for everything no route claims). Knows no harness concepts and serves no files; the composing application's frontend plugin owns dist serving through the fallback hook. … | owns `webServer` | 554 |
| `host-frontend-static` | Prov | fallback seat: serves the built frontend directory with the semantics the Web shell locked at step1 — traversal outside the dist root is 403, any miss falls back to index.html with HTTP 200 (SPA routing), unknown extensions ship as octet-stream, non-GET/HEAD is 405. … | injects `webServer` | 280 |
| `host-plugin-inventory` | Prov | Read-only projection of the current Cordis Loader plugin entries. | injects `loader` | 230 |
| `host-directory-picker` | **Def** | Service Definition for the `ctx.directoryPicker` capability seam: how the web-GUI host lets an operator select a workspace directory. Backends differ in interaction shape, not just mechanism, so the service exposes a discriminated capability instead of one method set: a `native` backend opens one OS … | owns `directoryPicker` | 206 |

#### api · remote BFF and RPC gateway (2 packages · 4,431 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `api-gateway` | Prov | Live Typert Remote dispatch over Cordis Services and registered providers. Transport, request correlation, and response envelopes belong to Connection. | injects `typert` | 3,586 |
| `api-remotes` | Prov | Host BFF entry and Loader shell for the Remote contribution assembly. | — | 845 |

#### typert · type-graph runtime (11 packages · 15,404 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `typert-generator` | Prov | Public API of the Typert analyzer, compiler-independent model, and model-driven artifact emitters. Build wiring lives in the `./tsdown` subpath. | — | 10,745 |
| `typert-registry` | Prov | Host entry for the shared Typert runtime registry. | — | 1,504 |
| `typert-loader` | Prov | Typert Loader integration: automatic registration for mounted plugin packages. When a loader entry mounts, this plugin resolves the entry's package.json; a package exporting `./typert` has its host face imported and its `TYPERT` manifest registered into `ctx.typert`, and the registration is withdraw … | injects `loader`, `typert` | 1,167 |
| `typert-protocol` | Prov | Remote decorators and explicit Gateway bindings backed only by private module state. Strict reflection remains a Typert compiler responsibility. | — | 1,104 |
| `@fixture/workspace` | other | — | — | 364 |
| `@fixture/host` | **Def** | — | owns `aliased`, `defaultOnly`, `demo`, `ignoredExternal`, `ignoredInline`, `ignoredPrimitive` | 305 |
| `@fixture/remote-workspace` | other | — | — | 78 |
| `@fixture/remote` | Prov | — | — | 55 |
| `@fixture/client` | **Def** | — | owns `clientBridge` | 40 |
| `@fixture/domain` | Prov | Host-only live Agent object. | — | 23 |
| `@fixture/write` | **Def** | Service whose public annotations are intentionally absent. | owns `writable` | 19 |

#### sdk · JSON-RPC protocol and clients (3 packages · 4,492 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `sdk-client` | Prov | TypeScript client SDK for the DeepSeek Harness runtime: spawn the `dsh-jsonrpc-agent` runtime as a subprocess and drive agent turns over stdio JSON-RPC. `DeepSeekHarness` is the high-level run API; `HarnessClient` is the lower-level protocol client. … | — | 1,953 |
| `sdk-jsonrpc-server` | Prov | SDK-facing JSON-RPC plugin over stdio. An external `cordis.yml` decides whether to load it; see the single-executable Agent Note and package README. Stdout is reserved for protocol frames, so the tree must not load a stdout logger. … | injects `agents` | 1,787 |
| `sdk-protocol` | Prov | Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport plus the named request, result, and notification types both wire ends speak. … | — | 752 |

#### acp · Agent Client Protocol (1 package · 1,669 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `acp` | Prov | Automation-only Agent Client Protocol server over JSON-RPC stdio. The bridge exposes fresh harness sessions to trusted programmatic clients. It carries prompt text, committed assistant text, cancellation, and one-shot permission decisions; presentation and human-interaction features stay with the ha … | injects `agents` | 1,669 |

#### settings · user settings (2 packages · 3,913 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `settings` | **Def** | Service Definition for the user-settings capability seam (`ctx.settings`). Providers store one raw document of per-namespace sections; plugins register a namespace schema and read the resolved value, which layers schema defaults, the registrant's composition `base`, and the user document section, in … | owns `settings` | 2,472 |
| `settings-file` | Prov | File-backed settings provider. One YAML or JSON document under the user's harness home carries every namespace section; external edits hot-publish through the seam, and every write re-reads the document under a cross-process writer lock before patching it as a comment-preserving leaf-level diff. | — | 1,441 |

#### credentials · credential references (2 packages · 1,831 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `credentials-local` | Prov | File-backed credentials provider over `$DSH_HOME/.credentials.yaml`, layered against the environment by how much each layer is trusted: ```text inherited process environment (read-only, wins) > $DSH_HOME/.credentials.yaml (provider-managed, writable) > <invocation cwd>/.env (read-only fallback) > $D … | — | 1,454 |
| `credentials` | **Def** | Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry *references* to secrets — environment-variable names — while providers own the actual values and their storage. … | owns `credentials` | 377 |

#### identity · anonymous identity (1 package · 252 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `anonymous-user-id` | Prov | Per-harness-home anonymous user id shared by telemetry and feedback. The id is a random UUID persisted as a bare line in `.anonymous-user-id` inside the harness home resolved by {@link resolveDshHome} (`$DSH_HOME` > `~/.dsh`), and never derived from the hostname, network address, git remote, or any  … | — | 252 |

#### storage · storage domains (4 packages · 3,428 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `storage-domain` | **Def** | Domain data form (`ctx.storage.domain`): schema-validated, change-emitting KV domains over storage backends. The single implementation of the domain layer — consumers depend on this package and never touch backends directly. … | owns `storageDomain` · injects `storage` | 1,475 |
| `storage-sqlite` | Prov | SQLite storage backend for the storage hub: one database file hosts every routed unit, document-per-row (`key TEXT` / `value TEXT` JSON). Registers as backend `sqlite`; the disposer unregisters first, then closes the medium. | injects `storage` | 766 |
| `storage-json` | Prov | JSON storage backend: one human-readable file per unit under a configured root, published by atomic whole-file rewrite. Registers as backend `json` on the storage hub. | injects `storage` | 658 |
| `storage` | **Def** | Storage hub (`ctx.storage`): a named backend registry plus mounted data-form facilities. The hub itself performs no IO — backends own media, data forms (the domain layer first) own semantics. | owns `storage` | 529 |

#### workspace · workspace registry (1 package · 2,155 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `workspace` | **Def** | Workspace entity registry (`ctx.workspaceRegistry`): durable workspace records, stable registry order, and header-validated session membership over the domain data form. | owns `workspaceRegistry` · injects `sessionPersistence`, `storageDomain` | 2,155 |

#### attachment · attachments (2 packages · 921 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `attachment-local` | Prov | Local durable attachment backend rooted below `DSH_HOME`. @module @deepseek-ai/dsh-attachment-local | — | 745 |
| `attachment` | **Def** | Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment | owns `attachments` | 176 |

#### feedback · message feedback (2 packages · 2,167 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `message-feedback` | **Def** | Durable, lifecycle-bound feedback for finalized assistant messages. | owns `messageFeedback` · injects `sessionPersistence`, `sessions`, `storageDomain` | 1,663 |
| `command-feedback` | Prov | Session feedback event plus the human-facing `/feedback` producer. Recording appends one authoritative log-only event and does not start model work. The append is eager but unflushed, so acknowledgement reports that the entry is logged, not that it reached disk. | injects `commands` | 504 |

#### runtime-diagnostics · runtime invariants (1 package · 543 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `invariants` | **Def** | Configurable registry for package-owned runtime invariant contributions. Every workspace package registers checks from a `./invariant` companion; ordinary package entrypoints stay independent of diagnostics. | owns `invariants` | 543 |

#### extensions · extension assembly (4 packages · 20,305 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `cordis-client-runner` | Prov | Dynamic-package runner plugin, node half. Pure browser-side capability: the empty apply exists so the row appears in the host cordis.yml / Loader, while the browser half ships through exports["./client"], discovered from the package.json dshClient declaration. | — | 6,988 |
| `tool-cordis` | **Tool** | Model-facing Cordis runtime/package inspection, define, run, stop, and remove tools. | injects `cordisInspect`, `dynamicCordisRunner`, `systemPrompt`, `tools` · tools `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop` +1 | 6,280 |
| `cordis-host-runner` | **Tool** | Dynamic Cordis Plugin service: immutable package definitions, one active run per Plugin, human-approved Client activation, and Host/Client invocation. | owns `dynamicCordisRunner` · injects `tools` · tool `console` | 4,975 |
| `client-ui-cordis` | Prov | Cordis dynamic-plugin card, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dshClient declaration. | — | 2,062 |

### 24.2 Domain VI guide: 39 frontend packages and “test infrastructure is a first-class citizen”

**`client` 39 packages · 138 163 lines** is the largest group in the warehouse, a quarter of the TS/TSX total. It is itself layered: ui-slots (in-degree 35) → ui-primitives (29) → runtime (34) → connection (19) → locale (27).

**`test-support` 6 packages · 13 344 lines** deserves a separate mention: test infrastructure is made into **publish-grade workspace packages** (e.g. `dsh-acp-snapshot` is a “suite factory”). Most projects’ test tools are scripts scattered in `tests/helpers/`; dsh treats them as product.

`vendor` 9 packages are pinned Cordis source (including `cordis-plugin-loader` / `-hmr` / `-include` / `-group`); `native` 4 packages are the Landlock launcher split by platform.

#### client · Web client modules (39 packages · 138,163 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `client-ui-conversation` | Client | Host registration for browser conversation preferences. | — | 21,302 |
| `client-runtime` | Client | Host loader entry for the browser runtime exported from `./client` and `./loader`. | — | 16,755 |
| `client-ui-primitives` | Client | Cordis-free React primitives styled only through `--dsw-*` tokens. | — | 12,011 |
| `client-ui-trajectory` | Client | Host loader entry for the browser-only trajectory plugin. | — | 11,400 |
| `client-connection` | Client | Host HTTP bridge for browser-client RPC. | injects `webServer` | 8,036 |
| `client-ui-settings-models` | Client | Host loader entry for the browser implementation exported from `./client`. | — | 7,463 |
| `client-ui-tool` | Client | Host loader entry for the browser-only Tool UI plugin. | — | 6,335 |
| `client-ui-workspace` | Client | Workspace picker plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader (load and lifecycle follow the host; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration). | — | 5,828 |
| `client-ui-agent-preset` | Client | Agent-preset surface plugin, node half. The empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships the General-settings row through exports["./client"], discovered from the package.json dsh.client declaration. | — | 4,688 |
| `client-ui-directory-picker-browse` | Client | Directory-picker browsing surface, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … | — | 3,213 |
| `client-ui-commands` | Client | Command UI plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … | — | 3,120 |
| `client-web-react` | Client | React bindings for the framework-neutral slot and snapshot contracts. | — | 3,096 |
| `client-ui-settings-plugins` | Client | Plugins settings surface, node half. The empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half owns the section and its configurable tab through exports["./client"], discovered from the package.json dsh.client declaration. … | — | 2,843 |
| `client-ui-input-trigger` | Client | Slash trigger plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | — | 2,835 |
| `client-ui-slots` | Client | Slot registry pure core. Owners declare slot contracts by merging into {@link SlotMap}; one `register` call contributes a component AND (optionally) declares child slots, a store seat, and the registrant's business face. Zero runtime dependencies (React types only). … | — | 2,322 |
| `client-ui-theme` | Client | Host registration for the browser theme preference and pre-plugin palette. | — | 2,054 |
| `client-ui-message-feedback` | Client | Message feedback surface plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | — | 1,914 |
| `client-ui-subagent` | Client | Subagent reference plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | — | 1,759 |
| `client-ui-settings-general` | Client | Host loader entry for the browser implementation exported from `./client`. | — | 1,688 |
| `client-ui-layout` | Client | Host loader entry for the browser-only layout plugin. | — | 1,560 |
| `client-ui-model-selection` | Client | Model selection plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | — | 1,455 |
| `client-modules` | Client | Node half of the client module system (`dsh.client` dual-face package): scans the host Loader's entries for packages declaring `dsh.client`, composes the `window.__DSH_BOOT__` entry graph (wire single source: {@link WebBootEntry} in `./client/manifest.ts`), serves `/plugins/<id>/client.js` and its s … | owns `clientModules` · injects `loader`, `webServer` | 1,440 |
| `client-ui-user-questions` | Client | Web question plugin, node half. Deliberately empty. Mounting `ask_user_question` here put it in the tools registry's GLOBAL layer, so every agent saw it no matter which preset composed it — a two-tool benchmark preset actually presented three, and a locally authored `bash-only` preset presented two. … | — | 1,390 |
| `client-locale` | Client | Host registration for the browser locale preference. | — | 1,290 |
| `client-ui-permission-presets` | Client | Permission surfaces plugin, node half. The empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships the new-session Settings row and current-session command picker through exports["./client"], discovered from the package.json dsh.client declaration. | — | 1,206 |
| `client-ui-workflow-run` | Client | Durable workflow-run UI plugin, node half. | — | 1,176 |
| `client-ui-goal` | Client | Goal surface plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | — | 1,093 |
| `client-ui-deliverables` | Client | Deliverables plugin, node half. Registers the response-format guidance that lets the browser half recognize final-response file references. The browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | injects `systemPrompt` | 1,029 |
| `client-ui-attachment` | Client | Pure React attachment atoms (zero cordis): the composer draft-image rail, the chat-history image gallery, the original-image lightbox, and the full-page drop overlay. Owners resolve every string through their own locale namespace and pass it down; nothing here reads application state. | — | 986 |
| `client-web` | Client | Web shell library entry. The shell's product is {@link AppWebEntry} — apps/web's vite entry runs it against #root; everything else (AppRoot gate, app-shell assembly entry, module-table staticModules, platform constants) is internal to the boot chain. … | — | 979 |
| `client-ui-skill` | Client | Skill reference plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. | — | 974 |
| `client-ui-settings` | Client | Host loader entry for the browser implementation exported from `./client`. | — | 961 |
| `client-ui-sidebar` | Client | Host loader entry for the browser-only sidebar plugin. | — | 957 |
| `client-ui-jobs` | Client | Background-job list plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dshClient declaration. | — | 663 |
| `client-hmr` | Client | HMR plugin, node half: the host end of the dev reload chain. One interval stat-polls every graph row's client bundle (polling by design: network mounts deliver no inotify events), reports content changes through `clientModuleHost.rebuilt(id)`, and serves the `/plugins/events` SSE channel broadcastin … | injects `clientModules`, `webServer` | 660 |
| `client-ui-settings-plugin-inventory` | Client | Host loader entry for the inventory-tab browser implementation exported from `./client`. | — | 563 |
| `client-ui-plan` | Client | Plan control plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … | — | 409 |
| `client-ui-directory-picker-native` | Client | Native directory-picker surface, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … | — | 391 |
| `client-schema-form` | Client | Schema/draft model layer for settings editors: rehydrate the wire's serialized schemastery envelope, resolve nodes by settings path, validate drafts, and edit them immutably by path. Editors render their own controls (the Models page hand-writes its layout) on top of these helpers. | — | 319 |

#### apps · product assembly (CLI / Web) (2 packages · 23,928 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `web-frontend` | Prov | Web application entry: thin bootstrap over the shell library. Everything — loader holding, module-table seeding, AppRoot gate, plugin assembly — lives in @deepseek-ai/dsh-client-web; this file only finds the mount point. | — | 20,359 |
| `dsh` | Prov | `dsh plugin --profile <name> <args...>` — profile plugin management as a thin pnpm forwarder: initialize the profile on first use, run `pnpm <args...>` in the profile directory, then reconcile the `dsh.profile.bundles` layer list against the installed state (a dependency resolving to a package that  … | — | 3,569 |

#### examples · runnable example assemblies (3 packages · 2,876 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `agent-spine-demo` | Prov | Default executor-less, UI-less agent spine. It bundles the common services, background-job registry and controls, optional persisted goals, concrete loop, local skill and agent-instructions providers, and model-facing shell/skill consumers; deployments still choose the LLM adapter, bash executor, an … | — | 1,854 |
| `acp-demo` | Prov | The ACP automation server app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo}), JSONL session persistence, and the {@link @deepseek-ai/dsh-acp} bridge. The app owns those plugins through one ordered lifecycle so ACP sessions quiesce before persistence detaches. … | — | 877 |
| `sdk-jsonrpc-demo` | Prov | Bin-only app package: its generic and packaged entries discover an external `cordis.yml` and own process exit. This module exports no composition plugin; the config chooses whether to load the {@link @deepseek-ai/dsh-sdk-jsonrpc-server} serving plugin. | — | 145 |

#### test-support · test infrastructure (6 packages · 13,391 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `acp-snapshot` | Lib | ACP snapshot suite kit — the shared machinery behind the keyless snapshot tier (`pnpm run test:snapshot`). Four layers, composable per example: the shared subprocess/client launcher ({@link launchAcpTestAgent}), the scripted scenario harness ({@link runScenario}), the pure expected-output normalizer … | — | 6,628 |
| `client-test-runtime` | Lib | jsdom slot test runtime: a real small runtime — Cordis `Context`, the runtime `SlotRegistry`, and the web-react renderer — assembled around test-owned session/workspace doubles, so feature specs exercise declaration, registration, scope, store, inject, rendering, updates, and disposal without hand-b … | — | 2,264 |
| `llm-replay` | Lib | Keyless snapshot-test LLM replay. It derives one model-call script per recorded session from `assistant/chunk` events and explicitly marked local compaction calls, then binds fresh live sessions to parent/child scripts by first-call order. … | injects `llm` | 2,085 |
| `llm-mock-server` | Lib | Scriptable OpenAI-compatible HTTP/SSE server for transport, protocol, and semantic-empty LLM recovery tests. Each accepted chat-completions request consumes one behavior; the server never retries or interprets harness policy. | — | 1,557 |
| `loader-smoke` | Lib | Shared subprocess harness for keyless example smokes that boot a real `cordis.yml` through an app bin and Cordis Loader. It also owns the mode-aware launch resolver every example subprocess harness shares ({@link resolveExampleLaunch}): booting an example bin from TypeScript source under `tsx` (the  … | — | 758 |
| `agent-loop-testkit` | Lib | Shared mounting for the services required before tests load the concrete agent loop. The caller retains ownership of the context, loop, adapters, optional plugins, and teardown. | — | 99 |

#### util · zero-dependency utility libraries (7 packages · 2,225 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `output-retention` | Lib | A dependency-light **retention** library: bounded model-facing output for tools that must cap how much context they return. A caller feeds items or text chunks into a bounded object, then gets the retained content plus exact omission metadata ({@link RetainedItems} / {@link RetainedText}). … | — | 852 |
| `timeout` | Lib | Shared timeout arithmetic, signal fusion, and classification. The library only notifies through abort signals; each capability still owns the mechanism that stops its work and translates timeout reasons into public outcomes. | — | 514 |
| `atomic-write` | Lib | Zero-dependency atomic file replacement and writer coordination. `writeFileAtomic` writes a random-suffix sibling with exclusive create and the caller's permission bits, then renames it over the target, so readers observe either the old or the new complete content and a replaced file ends up with ex … | — | 232 |
| `launch-environment` | Lib | Immutable launch-time environment snapshot that records which layer supplied each value. Harness consumers resolve through it instead of a flattened `process.env`; launchers may still materialize accepted values for config expressions and third-party libraries. | owns `launchEnvironment` | 226 |
| `home-paths` | Lib | Shared filesystem path helpers for DeepSeek Harness user data. | — | 221 |
| `native-command` | Lib | Shared no-shell `execFile` runner for host-native OS integrations (the native directory chooser, the open-with-default-application hand-off): utf8 stdio capture, abort propagation, Windows console hide. A library, not a plugin — no ctx, no state, no events. | — | 121 |
| `brand` | Lib | The `Branded<B>` nominal-typing primitive — a type-only utility (no runtime code, no harness-package dependency) shared by every package that owns a cross-boundary id. … | — | 59 |

#### vendor · vendored Cordis (9 packages · 6,584 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `cordis` | Prov | Core context type and root context implementation. | — | 2,702 |
| `cordis-plugin-loader` | **Def** | — | owns `entry`, `loader`, `startTime` | 1,161 |
| `schemastery` | Prov | — | — | 921 |
| `cordis-plugin-hmr` | **Def** | — | owns `hmr` · injects `loader`, `timer` | 614 |
| `cosmokit` | Prov | Array set and normalization helpers. | — | 483 |
| `cordis-plugin-include` | Prov | — | injects `loader` | 378 |
| `cordis-plugin-logger-console` | Prov | Re-export shared console exporter config and base implementation. | — | 173 |
| `cordis-plugin-timer` | **Def** | — | owns `timer` | 148 |
| `cordis-plugin-group` | Prov | — | — | 4 |

#### native · Landlock native launcher (4 packages · 343 lines)

| Package | Role | What it does (source `@module` JSDoc) | Wiring | Lines |
|---|---|---|---|---|
| `node-addon-landlock-run-workspace` | other | — | — | 215 |
| `node-addon-landlock-run` | Prov | The JavaScript API over the prebuilt `landlock-run` launcher: resolve the binary for this host, build its grant argv, and run its functional probe. This module owns the launcher's CLI contract (`docs/cli-contract.md`) so consumers never parse launcher output or spell launcher flags themselves — the  … | — | 128 |
| `node-addon-landlock-run-linux-arm64` | other | — | — | 0 |
| `node-addon-landlock-run-linux-x64` | other | — | — | 0 |

<h2 id="ch25">Chapter 25 · Collaboration graph: in-degree, assembly chains, plugin participation in one turn</h2>

### 25.1 In-degree layering

The dependency layers drawn from census data (arrow = is depended on):

```mermaid
flowchart BT
    INV["dsh-invariants ← 218 包<br/>（制度层：每包必有 invariant 伴生）"]
    SCH["schemastery ← 110 包<br/>（配置校验）"]
    BR["dsh-brand ← 30 包<br/>（branded id）"]
    TO["dsh-timeout ← 23 包"]
    SESS["dsh-session ← 84 包"]
    LLM["dsh-llm ← 82 包"]
    AG["dsh-agent ← 61 包"]
    TOOLS["dsh-tools ← 47 包"]
    SP["dsh-system-prompt ← 30 包"]
    CLIENT["dsh-client-ui-slots ← 35 包<br/>dsh-client-runtime ← 34"]
    API["dsh-api-remotes ← 23 包"]

    INV --- SCH --- BR --- TO
    SESS --> AG --> TOOLS
    LLM --> AG
    TOOLS --> SP
    API --> CLIENT
```

The three-layer structure is very clear:

- **Constitution layer** (`invariants` / `schemastery` / `brand` / `timeout`): cross-cuts every package, independent of business
- **Spine layer** (`session` → `agent` → `tools` → `system-prompt`, `llm` cross-cutting): the business core
- **Frontend layer** (`api-remotes` → `client-*`): an independent dependency community

### 25.2 Plugin participation table for one turn

This is the most direct answer to “how they cooperate” — in the same turn, at which moment each plugin joins:

| Moment | Event / hook | Who works here |
|---|---|---|
| Turn opens | `turn/start` (session event) | session persistence, telemetry, UI |
| Claim input | `inbox.claim` | user messages, tool additional context, jobs completion notices, skill-catalog injection |
| Assemble prompt | `systemPrompt.assemble` | 20 packages contribute sections: identity, tool schemas, cwd, skills list, todo, plan constraints, subagent-specific sections |
| Decide what the model sees | `agent/pre-step` ⟨waterfall⟩ | plan-mode intercept, context projection, permission pre-check |
| Fix request parameters | `agent/request` ⟨waterfall⟩ | model routing, reasoning effort, maxTokens, provider-specific parameters |
| Streaming | `llm/stream` ⟨waterfall⟩ | llm provider, token metering, CoT handling |
| Each slice | `assistant/chunk` (session event) | UI live rendering, persistence |
| Request failure | `agent/request-error` ⟨waterfall⟩ | retry policy, provider fallback |
| Before tool call | `tool/call` (session event) + `presentCall` | UI pending card |
| Tool admission | `tools/pre-execute` ⟨waterfall⟩ | hooks bridge, permission, sandbox policy, fs observation policy |
| Safety finality | monotonic guards | non-relaxable denial |
| Needs a human | `ctx.approval` | interaction group’s approval UI |
| Tool execution | `tools/execute` ⟨waterfall around⟩ | timeout, retry, metrics |
| Tool body | `ToolDefinition.execute` | one of the 24 Consumer packages |
| Mutation intent | `fs/write-intent` / `fs/edit-intent` | fs policy plugins |
| Result post-processing | `tools/post-execute` ⟨waterfall⟩ | spill (oversized persist), pruner (trim), result rewrite, additional context |
| Result finalized | `tools/result` ⟨emit · frozen⟩ | telemetry, UI completion card, invariant check |
| Step closes | `step/end` (session event) | stats |
| Stop announcement | `agent/turn-stopping` ⟨serial⟩ | same-turn objection (typical: `steer`); **not** goal’s cross-turn continuation |
| After idle | `agent/status` → `followup` | **goal-round-driver** opens the next round when idle + armed |
| Turn closes | `turn/end` (session event) | title generation, persistence flush, telemetry summary |

**This table is the value proof of a pluginized architecture**: 19 moments, each with an independent plugin joining, and of `agent.ts`’s 496 lines not one is written for “approval,” “sandbox,” “spill,” “goal,” or “title.”

### 25.3 Three typical assembly chains

**Chain one: the complete dependency closure of the `bash` tool**

```
tool-bash（Consumer）
  → ctx.tools（core/tools）        注册与流水线
  → ctx.shell（shell Definition）  接口
     → bash-local | bash-sandbox（Provider）
        → ctx.subprocess           起进程
        → ctx.sandbox              包 argv（sandbox 组）
        → ctx.sandboxPolicy        策略
  → ctx.systemPrompt               贡献工具 schema + 用法段
  → ctx.shellEnv                   托管 DSH_* 环境
  → ctx.jobs（call time）          run_in_background
  → ctx.approval（间接）           写操作审批
```

One tool, seven service keys, three provider layers. **And its source is only 2 081 lines**, because it does not need to care about any of those layers.

**Chain two: a legal path for “add context for the model”**

```
某插件 → agent.inject(message)
  → inbox（不唤醒）
  → 下次有唤醒消息时一起 claim
  → append user/message（session 事件）
  → deriveMessages() 投影进模型历史
  → invariant 断言：模型可见 ⟺ 已记录 ✓
```

**Chain three: switch to a remote execution world**

```
patch 掉两行配置：
  fs-local     → e2b-fs
  subprocess-local → e2b-subprocess
自动跟着搬家：bash / pwsh / 持久 PTY / lsp / glob / grep / code-runtime
零 provider fork
```

---

<div class="part-band"><span class="band-k">Part VII</span>Engineering constitution: the strongest set this series has seen</div>

<h2 id="ch26">Chapter 26 · Per-file 100% coverage: coverage as a deletion tool</h2>

### 26.1 Six test layers

`docs/testing.md` (49 lines, all substance):

| Layer | Command | Key requirement |
|---|---|---|
| Unit | `pnpm run test` | **Every registry must have an HMR-safety test** (dispose the contributing fiber, assert cleanup is clean); prefer permanent tests of boundaries, error paths, event order, concurrency races, contract regressions |
| **Coverage gate** | `pnpm run test:coverage` | **Per-file 100% on `packages/*/*/src`** |
| Real-API e2e | `pnpm run test:e2e` | Hit real providers with a key; each suite self-skips by its own key |
| **Keyless snapshot** | `pnpm run test:snapshot` | Boot a real example, replay a recorded session, diff normalized JSON-RPC + the re-persisted log |
| Browser snapshot | `pnpm run test:web` | Chromium replay, **required on Linux PRs**; CI forces read-only `DSH_SNAPSHOT=replay` |
| Gate collection | `doc-sync` / `hygiene` / `duplication` | see Chapter 27 |

### 26.2 Technical highlight: the correct reading of coverage

`docs/testing.md:10`:

> **An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on.** Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped.

**“An uncovered line is often dead code; the gate is correctly hinting that you should delete it”** — treat coverage as a **code-deletion tool**, not a write-tests KPI.

This explains the `/* v8 ignore next -- … */` seen in Chapter 8: an unreachable defensive branch must be explicitly exempted + the reason written down, and `scripts/coverage-exempt.ts` + `coverage-exempt.spec.ts` guard the exemptions themselves.

It also explains the “do not over-defend” rule in §2.2 — under a 100% coverage constitution, **every surplus defensive branch has a direct cost**.

### 26.3 An honest exception

`docs/testing.md:10`:

> Per-file 100% on `packages/shell/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` **exempts the file** so pwsh-less hosts stay green, while **CI runners ship pwsh and enforce the full bar**.

On a machine without PowerShell the file is exempted so the host stays green; CI ships pwsh and enforces full marks. **The exception is written into the docs with a reason**, not a silent lowering of the standard.

### 26.4 Three test rules I think most worth copying

**① Verify the world, not the self-report** (`:29`):

> An e2e assertion **re-runs the command or re-reads the file externally**; **a keyword probe on the agent's own output lets a cheating agent pass**. Assert untouched files are byte-identical.

“A keyword probe on the agent’s own output lets a cheating agent pass” — that sentence should be carved at the top of every agent project’s test file.

**② A guard must be proven to work** (`:34`):

> A guard only guards if the regression actually fails it. … add an explicit assertion, **and prove it: introduce the regression, watch red, revert.**

**First introduce the regression, watch it go red, then revert** — this is the highest testing standard I have seen in this series.

**③ Test the real entry path** (`:35`):

> "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` **under plain `node`**, exposing failures tsx masks (**settle races, module resolution, swallowed load failures**).

Running source under tsx will mask three classes of real failure. The critical path must be tested with the **published artifact**.

### 26.5 The with-key policy: a sentence that carries company identity

`docs/testing.md:19`:

> **The with-key policy: inference is cheap here** — **We are DeepSeek — do not ration real-API tests.** A no-key test proves plumbing; only a with-key run proves the agent works against a real model. … **Self-skip keeps secretless CI and keyless contributors unblocked; it is not a cost signal.**

“We are DeepSeek; do not ration real-API tests” — this is the most personality-bearing sentence in the warehouse. And it explicitly says: self-skip is so that keyless contributors are not blocked; **it is not a cost signal**.

Accompanying it is the judgment that “highest value is a smoke test”:

> Highest-value are **smoke tests** that boot the real example, send one prompt, and check the world — they catch the "**green unit tests, broken product**" class that mocks cannot（postmortem 0001）.

---

<h2 id="ch27">Chapter 27 · 28 `verify-*` gates: welding spoken convention into machine gates</h2>

### 27.1 Constitutional requirement

`AGENTS.md`:

> **Wire mechanically checkable invariants into an executed top-level gate and prove each changed acceptance path rejects an invalid case.** Use narrow, justified exceptions instead of disabling a rule globally.

Two requirements: a mechanically checkable invariant **must be wired into a top-level gate that will actually execute**; and you must **prove that every changed acceptance path rejects illegal input**.

### 27.2 Gate list

124 `.ts` files under `scripts/`, of which 28 are `verify-*`:

| Gate | What it turns into a machine-checkable fact |
|---|---|
`verify-export-jsdoc` | every export has JSDoc; function-class exports have `@param`/`@returns`
`verify-cordis-config` | a bare plugin in `cordis.yml` must be in the resolver manifest’s `dependencies`
`verify-package-invariants` | every package has an `./invariant` companion; an empty installer must explain with `No runtime invariant:`
`verify-built-package-invariants` | the same on the artifact side
`verify-type-equiv` | **`ts type-equiv` code blocks in docs must be type-equivalent to source**
`verify-mermaid` | diagrams must render
`verify-md-links` / `verify-doc-refs` | links and refs must not rot
`verify-md-wrap` | markdown wrap discipline (one physical line per paragraph)
`verify-doc-budgets` | **document word-count budgets** (over the limit, raise the manifest’s cap)
`verify-doc-site-fragments` | doc-site fragments
`verify-package-readme-limitations` | **every package README must state “limitations”**
`verify-package-readme-model-experience` | **must state “model experience”**
`verify-agent-note-classification` / `-format` | classification and format of design docs
`verify-archived-agent-notes` | freeze rules for archived notes
`verify-translation-pairing` / `-prompt` | bilingual docs paired paragraph by paragraph
`verify-config-source-ownership` | ownership of config sources
`verify-client-domain-graph` | frontend domain graph
`verify-runtime-closure` | runtime dependency closure
`verify-node-next-types` | NodeNext consumer types
`verify-package-paths` | package paths
`verify-dsh-package-licenses` | licenses
`verify-public-repository-links` | public repository links
`verify-skill-invocation-metadata` | skill invocation metadata
`verify-vendored-links` | vendored links
`verify-md-*` etc. | remaining document discipline

Also `pnpm run hygiene` (knip dead code + publint publish norms + workspace constraints + NodeNext consumer checks) and `pnpm run duplication` (jscpd cross-file TypeScript clone detection).

### 27.3 Three this series has not seen a second house do

**① `verify-package-readme-limitations`**: forces every package README to state **limitations**. This guards against “over-advertising” — if a package’s docs only say what it can do, not what it cannot, users will use it in the wrong scene.

**② `verify-package-readme-model-experience`**: forces a statement of **model experience**. It requires the author to describe their package from the model’s point of view: what the model will see, how it will misuse it, what behavior the schema wording will cause. **This is the institutionalization of “write docs for the model.”**

**③ `verify-type-equiv`**: type code blocks in docs must be **equivalent** to source types. So those `ts type-equiv` fenced blocks in `docs/subsystems/*.md` are not hand-copied; they are machine-checked against source.

### 27.4 The gates themselves are guarded

Under `scripts/` you see files appearing in pairs: `coverage-exempt.ts` + `coverage-exempt.spec.ts`, `run-gates.ts` + `run-gates.spec.ts`, `gen-doc-graphs.ts` + `gen-doc-graphs.spec.ts`, `translation-pairing.ts` + `.spec.ts`…

**Guards must be guarded.** A broken gate is more dangerous than no gate, because it gives a false sense of safety.

### 27.5 8 177 lines of generated documentation

These in `docs/` are **generated** (produced by `scripts/gen-*.ts`, freshness verified by `doc-sync`):

| Document | Lines | Generation method |
|---|---|---|
`config-catalog.md` | 3 151 | Config fields of every plugin
`tool-catalog.md` | 1 873 | **boot each tool plugin and read `ctx.tools.schemas()`**
`module-graph.md` | 1 638 | module graph
`persistence-catalog.md` | 944 | persistence catalog
`capability-seams.md` | 471 | capability graph
`event-producer-consumer.md` | 76 | event producers/consumers
`graph-atlas.md` | 24 | graph index

Plus the “Cordis API” blocks on every subsystem page (generated by `gen-cordis-catalog.ts`, **byte-identical on both language sides**).

**Conclusion**: this repository’s documentation is not “written”; it is **generated from source + machine-checked for freshness**. That is also why I could extract accurate duties for 241 packages with one census script — **because JSDoc in source is forced by a gate**.

---

<h2 id="ch28">Chapter 28 · 686 Agent Notes: institutionalizing the archaeology of design decisions</h2>

### 28.1 Numbers

English primary files under `.agents/notes/` (each also has a `.zh.md` + `.i18n.yaml` three-piece set):

| Lifecycle | Count | Meaning |
|---|---|---|
| `implemented/` | **507** | the decision has shipped, and **is kept in sync with what actually shipped** |
| `archived/` | 143 | completed and the rationale no longer guides future work, **frozen** |
| `proposed/` | 25 | proposals reviewed before implementation |
| `rejected/` | 11 | considered then rejected, **kept only when the rationale prevents a tempting mistake** |
| **Total** | **686** | |

### 28.2 What it is

`.agents/notes/README.md`:

> An **Agent Note** records a decision or proposal that affects this codebase — **the *why* and *what we gave up***, the parts code and docs can't carry.

**“Why” and “what we gave up”** — this is exactly what this series’ analysis methodology (motive → constraint → rejected alternatives → choice → cost) tries to reverse-engineer from code. **dsh writes it down at the source.**

### 28.3 Dual-axis path encoding

```
{lifecycle}/{class}/yyyy-mm-dd-topic-title.md
```

- **lifecycle** (top-level directory) = state; notes **move between directories** as state changes
- **class** (nested directory) = the **kind** of decision, from a closed set (defined by `scripts/agent-note-tree.ts`; the classification gate rejects other folders):

| class | What it covers |
|---|---|
`feature` | a new user- or model-visible capability
`bug-fix` | fix a defect or a hole a postmortem exposed
`simplification` | delete code/behavior/surface without adding capability
`architecture` | a structural decision about **shipped source**
`process` | tools/policy/process **around** the code (gates, package manager, vendoring)
`testing` | test infrastructure and policy

**`refactor` is deliberately absent**; the README gives the reason:

> (`refactor` is deliberately absent — it overlaps `simplification`, whose discriminator, "**does observable behavior change?**", already covers it.)

Use one **discriminator** (does observable behavior change?) to kill a fuzzy category. This is good taxonomic taste.

### 28.4 Four careful rules

**① implemented must stay in sync with what actually shipped:**

> is **kept current with what actually shipped**: when the code later moves a file, renames a package, or changes a key/default, the Agent Note is updated in the same change to match (**facts only** — paths, names, structure — **not the decision itself**).

Only facts are updated (paths, names, structure), **not the decision itself**. So when you read a 2026-06 note, the paths in it are current, but the decision is of that time.

**② archived is frozen:**

> Archived notes are frozen: **never edit or treat them as current authority**.

When archiving, the only allowed operations: move the three-piece set, keep `Status: implemented`, insert the same `Archived: YYYY-MM-DD` line below the status (in both languages), re-record the sidecar, fix or delete inbound links. **This is the only allowed content change.**

**③ Cross-references must be relative markdown links:**

> Cross-references between Agent Notes use relative markdown links … — **never bare prose or numbers** — so they are **mechanically checkable and survive moves between folders**.

You may not write “see that note about X”; you must write a link. Then `verify-doc-refs` can check, and after a note moves between lifecycle directories the link can still be repaired.

**④ Do not add a centralized index:**

> **Do not add a centralized `INDEX.md`**; the [no-index Agent Note] owns the rationale.

**Even the decision “do not add an index” itself has an Agent Note.** This is the extreme of constitutional self-reference.

### 28.5 Cooperation with AGENTS.md

`AGENTS.md`:

> **Non-trivial changes MUST include an Agent Note in the same PR;** only mechanical/local edits are exempt.

And `verify-agent-note-classification` / `verify-agent-note-format` / `verify-archived-agent-notes` three gates guard this constitution.

### 28.6 11 skills for AI collaborators

`.agents/skills/`:

| skill | Role |
|---|---|
`dsh-prose-standard` | prose standard (decides how words such as “contract/boundary/shape” are used)
`dsh-pre-push-checks` | which checks to run before push
`dsh-code-review` | code review
`dsh-doc-standards` / `dsh-doc-site-sync` | document standards and site sync
`dsh-translate-docs` | bilingual translation (**may run only when the user explicitly invokes it**)
`dsh-archive-agent-notes` | a **calibrated** workflow for archiving notes (the README is explicit: use this rather than word count, age, or quota)
`dsh-find-simplifications` | find places that can be simplified
`dsh-merging-stacked-prs` | stacked-PR merging
`dsh-trim-cot-leakage` | **trim CoT leakage**
`record-browser-gif` | record a browser GIF

### 28.7 A rare prose discipline

The “Type safety and documentation” section of `AGENTS.md`:

> Comments and docs state complete contracts and context, **not reasoning transcripts**. Use direct, concrete terms. **Do not use metaphors.** Before writing `contract`, `boundary`, or `shape`, **ask whether a more exact term names the subject**: write `response fields`, `JSON validation`, or `ESM exports` instead of `response shape`, `validation boundary`, or `module shape`.

**“Do not use metaphors”** + **“before writing contract/boundary/shape, ask whether a more exact term names the subject”** — it even gives three pairs of replacement examples, and stipulates that `contract` is reserved for obligations a caller actually depends on: preconditions, postconditions, invariants, compatibility promises.

Also:

> **Do not narrate control flow or tests, preserve review history, or restate code.** Keep behavior, failure, timing, ownership, and safe-use facts; link the rationale.

**Do not narrate control flow, do not preserve review history, do not restate code.** Keep five classes of fact — behavior, failure, timing, ownership, safe use — and link the rationale.

This prose discipline plus `verify-doc-budgets` (word-count budgets) explains why this repository’s 40-plus subsystem documents read at extremely high density without being verbose.

---

<h2 id="ch29">Chapter 29 · Dynamic Cordis: the agent mounts its own plugins</h2>

### 29.1 What it is

`AGENTS.md`’s repository layout still writes `self-modification/`. **The repository has no such directory.** `vitest.config.ts`’s coverage exclude still lists `packages/self-modification/*/src/**`. The actual code is in `packages/extensions/`:

| Package | Duty |
|---|---|
| `dsh-cordis-host-runner` | `ctx.dynamicCordisRunner`: define / run / stop / undefine |
| `dsh-tool-cordis` | 7 tools exposed to the model |
| `dsh-cordis-client-runner` / `dsh-ui-cordis` | Client half and UI |

Corresponding tools (the census captured 7):

```
cordis_define · cordis_undefine · cordis_run · cordis_stop
cordis_inspect_list · cordis_inspect_query · cordis_inspect_self
```

There is a demo script in `package.json`: `pnpm run demo:cordis` — **"the agent modifies its own runtime"**.

### 29.2 Safety boundary

The deployment note for `dsh-tool-cordis` in `docs/tool-catalog.md` is cautious:

> **Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime**, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry **and the vm sandbox**; a composition missing it never activates the tools. **A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes.**

Five safety designs:

1. **Not in any shipped tree** — a deliberate opt-in, because dynamic package code reaches the real runtime
2. Needs `ctx.dynamicCordisRunner` (from an independent host-runner package), which holds the definition registry **and the vm sandbox**
3. If the composition is missing this service, the tools **never activate**
4. A running package may register **additional model-visible tools** — until it is stopped / undefined / DSH restarts
5. **Tool-set changes are recorded in the log as a full changed request header** — back to Chapter 10’s constitution: the tool table is model-visible, so its changes must already be logged

### 29.3 Why this is the logical endpoint of “everything is a plugin”

If everything is a plugin, and the agent can write code, then the agent can write plugins. And if registration is a reversible effect, then a plugin the agent mounts can also be cleanly unloaded.

**This capability is not bolted on; it is derived from the architecture.** It is also a stress test of the architecture: if dynamically mounting a plugin would break something, that something should not be privileged state.

The closest in this series are DeepTutor’s subagent (treat a coding CLI as a KB) and Open Design (treat a CLI as the engine), but those two are “call an external agent.” This one in dsh is **the agent modifying its own runtime assembly** — a different order of magnitude.

---

<div class="part-band"><span class="band-k">Part VIII</span>Taste and bounds</div>

<h2 id="ch30">Chapter 30 · Sixteen decisions, five-beat recap</h2>

| # | Decision | Motive | Rejected alternative | Choice | Cost |
|---|---|---|---|---|---|
| 1 | Everything is a plugin, no privileged kernel | Every new requirement patches the main loop | Main loop + hooks | Cordis service warehouse; the loop is also a plugin | Steep learning curve; many layers of indirection |
| 2 | Vendored Cordis | Framework and product evolve on the same cadence | npm dependency, lock a version | Pinned source copy + manifest + sync procedure | Sync must re-run test/build; local patches must be re-applied |
| 3 | `inject` expresses load order | A manual boot-order table is unmaintainable | Explicit boot sequence | Fixed-point resolution + error if not activated | Diagnosing “why didn’t it load” needs new tools |
| 4 | Four dispatch modes | One EventEmitter cannot hold four semantics | Unified emit | `@mode` written into the public contract + generated catalog checks | Authors must judge which to use |
| 5 | waterfall MUST call `next()` | An observer short-circuit silently disables everything | Convention | Capitalized MUST + docs + tests | Still human-guarded (no static check) |
| 6 | Seam three roles | Swapping an implementation should not fork the tool | interface + if/else | Definition/Provider/Consumer; one role is not a seam | A capability is at least three packages; warehouse is 241 packages |
| 7 | request/spec split | Implicit defaults cannot be logged or asserted | `?? default` in `run()` | Explicit `resolve(request): Spec` | One extra type and one conversion |
| 8 | Append-only log as truth | In-memory messages lose everything | Array + incidentally log | Event sourcing + `deriveMessages()` | Log volume is large; adding context requires extending the event table |
| 9 | Model-visible ⟺ logged | Bypassing the log silently distorts | Rely on review | 218 packages’ invariant companions + attributable failure | Real maintenance burden |
| 10 | Turn/step two-level boundary | One level cannot say “is something owed” | Single-layer while | step = one request + tools; turn = until nothing is owed | Two counters and two delivery slots |
| 11 | `agent/turn-stopping` announcement | Same-turn continuation should not change the loop | Add a condition in the loop | serial announcement + `steer` + second inbox check | One extra event round-trip at stop; **cross-turn goal continuation separately walks idle + `followup`** |
| 12 | Monotonic guards | waterfall can be privilege-escalated | Waterfall only | Guards can only deny or abstain | Two admission layers; authors must pick the right one |
| 13 | Result freeze + emit | An observer secretly mutates a finalized result | Unified waterfall | `Readonly` + `Object.freeze` + invariant | Post-processing must finish in post-execute |
| 14 | Configuration is architecture (bundle/profile/patch) | Multiple frontend shapes cannot have three `main`s | Environment variables + if | Ordered overlay + whole-row replace + `--dump-config` | A patch is a whole-row replace, verbose to write |
| 15 | Per-file 100% coverage | Dead code and fake tests | Overall percentage | Per-file full marks + reasoned exemptions | Needs an exemption mechanism and its guards |
| 16 | 686 Agent Notes | Code and docs cannot carry “why” | Rely on PR descriptions | Dual-axis path + closed classification + three gates | Every non-trivial change costs one more document |

---

<h2 id="ch31">Chapter 31 · Cross comparison: harness-shaped vs loop-shaped</h2>

### 31.1 Comparison with the main projects in this series

| Dimension | DeepSeek Harness | Reasonix (same company) | Claude Code | Codex | Open Design | DeepTutor |
|---|---|---|---|---|---|---|
| Architectural shape | **harness (no kernel)** | single binary + transport-agnostic controller | loop-shaped | loop-shaped | host-shaped (does not write a loop) | dual-track loop + three-layer plugins |
| Status of the main loop | **a replaceable plugin** | core, single loop | core | core | does not write one | core (two) |
| Extension method | mount plugins onto 73 service keys | MCP + `use_capability` | tools / permissions / hooks | tools / sandbox policy | adapter as data | three-layer plugins |
| Context truth | **append-only log + projection** | stable prefix + turn tail | session + compaction | session | playbook | UnifiedContext |
| Capability replacement | **seam three roles; swap a provider, the whole world moves** | provider adapters | limited | limited | swap CLI | swap RAG engine |
| Permission model | pre-execute + **monotonic guards** + approval | application layer | permission rules | OS three-platform fences | none (patched at the periphery) | ToolMountFlags + sandbox grades |
| Sandbox | `confine(argv)`: Linux bwrap→Landlock (C11 launcher); macOS Seatbelt; Win ACL | application layer + OS | permissions | Seatbelt/Landlock/Token | — | three backends, two grades |
| Engineering gates | **per-file 100% + 28 verify + 686 notes** | conventional | conventional | conventional | conventional | conventional |
| Package count | **241** | single binary | — | — | — | 219 (Python) |
| License | MIT | MIT | various | various | depends on the repo | Apache-2.0 |

### 31.2 Two roads from the same company

**Reasonix**: one static binary, cache-first, a transport-agnostic `control.Controller`, three-stage compression soft→snip→summary. The goal is **save money + portable**.

**Harness**: 241-package monorepo, everything is a plugin, 73 service keys, six test layers. The goal is **replaceable + extensible + long-term maintainable**.

This is not a contradiction; it is two different product hypotheses: Reasonix assumes “the user wants a terminal agent that can be stuffed into any environment”; Harness assumes “the ecosystem wants a skeleton that can be reshaped into any form.”

### 31.3 Applicability bounds of the harness shape

**The harness shape wins at**: one behavior across multiple frontends, third-party extension, whole-capability replacement (host↔remote), long-term maintainability, welding discipline into gates.

**The loop shape wins at**: fast onboarding, intuitive debugging (you can see the whole process in the stack), low cost of a small change, no assembly layer to maintain.

A concrete criterion: **does your product need to support “the user replaces the execution environment”?** If yes (remote sandbox, enterprise intranet, custom runtime), the seam investment of the harness shape will pay back. If not, the 241 packages of indirection are a pure tax.

### 31.4 Three combinations nobody else has

1. **`dsh-invariants` is depended on by 218/241 packages** — runtime invariants are part of a package’s birth certificate, and it specifies “you may not write an assertion that pretends to check.”
2. **Monotonic guards** — using the type (no `allow` return value) to guarantee that a safety policy cannot be mathematically relaxed by a later plugin.
3. **686 dual-axis-classified Agent Notes + 3 gates** — institutionalizing “why we designed it this way, what we gave up” to the point of having a lifecycle, a closed classification, archive-freeze rules, and even “do not add an index” having a note.

---

<h2 id="ch32">Chapter 32 · Honest bounds</h2>

1. **It is a developer preview.** The README warns in capitals “THERE WILL BE COMPATIBILITY-BREAKING CHANGES”; `SESSION_FORMAT_VERSION` stays at `0` and **explicitly makes no compatibility promise**; `AGENTS.md` has a dedicated “pre-release stance” section and wrote its own deletion condition. **Using it in production now requires an upgrade budget.**
2. **The volume is huge**: 497k lines of TS + 67k lines of TSX + 241 packages. This analysis grabs the constitution, the spine, the collaboration graph, and the institutional system; **it is not a per-package deep read** — client 39 packages 138k lines, typert 11 packages 15k lines, extensions 4 packages 20k lines all received only roster-level coverage.
3. **The learning curve is a real cost.** Understanding one request requires simultaneously loading the service warehouse, inject resolution, four dispatch modes, the effect lifecycle, the seam’s three roles, and assembly layering — six things. The architecture document itself says “**We recommend using an agent to explore the codebase**” (`docs/architecture.md:7`) — **it recommends you use an agent to read it**.
4. **Indirection makes debugging harder.** To know who `ctx.shell` is you look at assembly, not code. That is why the whole navigation toolkit of `--dump-config`, `cordis_inspect_*`, `module-graph.md` exists — **the existence of these tools is itself evidence of the cost**.
5. **Maintenance cost of the `verify-*` gates.** 28 gates + each one’s `.spec.ts` + 124 scripts; this system needs people to keep feeding it. For a small team it is a net burden.
6. **The roster’s role classification is my inference.** The census script classifies by “package-name prefix + whether a ctx key is declared + whether a tool is registered,” which may differ from the repository’s own grouping in `packages/README.md`; **the duty-description column is source JSDoc original wording**, higher credibility.
7. **`inject` extraction is incomplete.** Cordis declares dependencies in several ways (`export const inject`, `static inject`, Service constructor); my script covers the first two, so “101 packages identified as having inject” may be an underestimate. **The inter-package-dependency column (229 packages have it) is more reliable.**
8. **BENCHMARK.md is almost empty.** The whole text only says “follow the Python SDK guide, run the `jsonrpc-agent` minimal variant, use an independent workspace and session id.” **This repository’s quality confidence comes from gates and snapshots, not from a public scoreboard.**
9. **Runtime behavior has not been verified.** This analysis is static reading + cross-checking docs against source; I did not actually run `pnpm dsh web` (needs an API key and a full build). Sequence diagrams and pipeline diagrams are derived from source and generated docs.
10. **Line numbers are anchored at `47f9438`.** This is a repository with extremely high daily-update frequency (HEAD is that day’s merge commit; PR numbers have already reached #2519); **line numbers will drift; when re-checking, take symbol names and file names as the authority**.

---

<h2 id="ch33">Chapter 33 · Source tour index and local reproduction</h2>

### 33.1 🔍 Source pointer table

| You want to understand… | Read first |
|---|---|
| Architectural constitution (for humans and AIs) | `AGENTS.md` (root; `CLAUDE.md` is its symlink) |
| Architectural panorama | `docs/architecture.md` (129 lines, required reading) |
| Cordis’s five ideas | `docs/cordis-primer.md` (44 lines, 5 minutes) |
| Strict definitions of terms | `docs/glossary.md` (seam / scope / lineage…) |
| turn/step loop | `packages/core/agent-loop/src/agent.ts:212, 246, 332, 407` |
| Agent interface and inbox | `packages/core/agent/` |
| Session log and projection | `packages/core/session/` + `packages/session/` (13 packages) |
| Tool registry and pipeline | `packages/core/tools/src/index.ts`; diagram in `docs/tool-execution-pipeline.md` |
| Pipeline-order invariants | `packages/core/tools/src/invariant.ts:94-115` |
| System-prompt assembly | `packages/core/system-prompt/` |
| per-agent scope | `packages/core/scope/` |
| Seam template (three roles) | `packages/shell/` (9 packages: 1 Definition + 4 Provider + 3 Consumer + 1 env) |
| One execution world | `packages/fs/` + `packages/subprocess/`; empirical proof `packages/e2b/` |
| Sandbox | `packages/sandbox/` + `native/landlock-run/` |
| Context economics | `packages/compaction/` `packages/spill/` |
| Goal continuation | `packages/goal/` |
| Sub-agents | `packages/subagent/` (11 packages, including codex / claude-code providers) |
| Code Mode | `packages/code-runtime/`; notes in `run_code` of `docs/tool-catalog.md` |
| Type-graph RPC | `packages/typert/` (11 packages) + `packages/api/`; `docs/api-gateway.md` |
| Assembly layering | `packages/boot/` `packages/bundle/` `packages/preset/` |
| Invariant constitution | `docs/subsystems/invariants.md` + `packages/runtime-diagnostics/invariants/` |
| Testing constitution | `docs/testing.md` (49 lines, all substance) |
| Gate scripts | `scripts/verify-*.ts` (28) + `scripts/run-gates.ts` |
| Design-decision archaeology | `.agents/notes/{proposed,implemented,archived,rejected}/` (686 notes) |
| Workflows for AIs | `.agents/skills/` (11 SKILL.md) |
| Incident postmortems | `docs/postmortem/0001-0004` |
| Generated catalogs | `docs/{config,tool}-catalog.md` `module-graph.md` `capability-seams.md` `persistence-catalog.md` |
| Full schemas of the 47 tools | `docs/tool-catalog.md` (1 873 lines) |

### 33.2 Local reproduction (read-only analysis)

```bash
cd 参考项目/deepseek-harness
git rev-parse --short HEAD          # 期望 47f9438
git log -1 --format='%s'            # Merge pull request #2519 …

# 复核本文的关键数字
find packages -name package.json -not -path '*/node_modules/*' -not -path '*/lib/*' | wc -l   # 219
find . -name '*.ts' -not -path '*/node_modules/*' -not -path './.git/*' | wc -l               # 2319
ls scripts/verify-*.ts | wc -l                                                                # 28
for s in proposed implemented archived rejected; do
  printf "%s: " "$s"; find ".agents/notes/$s" -name '*.md' -not -name '*.zh.md' | wc -l
done                                                                                          # 25/507/143/11
```

Reproduce this lecture’s 241-package roster (scripts in this repository):

```bash
cd "claude code源码分析"
python3 scripts/dsh-plugin-census.py     # → scripts/dsh-plugin-census.json（241 包）
python3 scripts/dsh-roster-gen.py        # → scripts/dsh-roster.md（分组名册）
```

If you actually want to run it (not required for analysis; needs `DEEPSEEK_API_KEY`):

```bash
pnpm install                             # node ^22.19 || >=24
pnpm run build
pnpm dsh web                             # http://127.0.0.1:3080
pnpm dsh --profile web --dump-config     # 看实际装配的整棵树
pnpm dsh --profile headless "一句任务"
pnpm run demo:cordis                     # agent 修改自己的运行时
```

### 33.3 Handoff to other documents in this series

- Want to build one by hand → [Build a pluginized agent harness from scratch](../从零构建插件化AgentHarness-DeepSeekHarness系-开发全流程教程.md) (20 steps · minidsh)
- The same company’s other road → [DeepSeek-Reasonix source analysis](./DeepSeek-Reasonix-源码分析.md) (single binary + cache-first)
- The extreme of not writing a loop → [Open Design source analysis](./open-design-源码分析.md) (treat someone else’s CLI as the engine)
- The classic of the loop shape → [How Claude Code works (deep dive)](../Claude-Code-工作原理科普-深入版.md)
- Sandbox and permission contrast → [openai-codex source analysis](./openai-codex-源码分析.md) (three-platform OS fences)
- Three-layer plugins and the education axis → [DeepTutor source analysis](./DeepTutor-源码分析.md)
- Engineering-technique cross-cut → [Catalog of agent engineering patterns](../Agent工程模式目录.md)

---

## Appendix A · Version-baseline fingerprint

| Item | Value |
|---|---|
| Repo | https://github.com/deepseek-ai/deepseek-harness |
| Version / Commit | `v0.1.0-rc.5` (`package.json`) / `47f9438` |
| License | MIT |
| Total workspace packages | **241** (`packages/` 219 + vendor 9 + apps 2 + native 4 + others) |
| TypeScript | 497 489 lines / 2 319 files |
| TSX | 66 633 lines / 259 files |
| Python | 4 286 lines / 19 files |
| `ctx` service keys | **73** |
| Model-visible tools | **47** |
| Service Definition packages | 60 |
| Consumer (tool) packages | 24 |
| Provider/plugin packages | 97 |
| Largest group | `client` 39 packages · 138 163 lines |
| Largest single package | `core/tools` 13 743 lines |
| In-degree champion | `dsh-invariants` ← **218 packages (90%)** |
| `verify-*` gates | **28** (124 `.ts` files under `scripts/` in total) |
| Agent Notes | **686** (507 implemented / 143 archived / 25 proposed / 11 rejected) |
| Generated documentation | 8 177 lines |
| postmortem | 4 notes |
| `.agents/skills` | 11 |
| Substrate | vendored Cordis (9 packages · 6 584 lines) |
| Coverage gate | `packages/*/*/src` **per-file 100%** |

## Appendix B · Three most distinctive designs (for catalog cards)

1. **No privileged kernel**: the model adapter, the tool registry, the session log, **and even the agent loop itself** are plugins; 73 `ctx` service keys are replaceable from configuration; every row `--dump-config` prints can be swapped by a patch of your own.
2. **“May mutate” and “may only watch” are separated at the type level**: three waterfalls may rewrite; `tools/result` is `emit` + `Readonly` + `Object.freeze` + a runtime invariant; the safety boundary additionally uses **monotonic guards** (the return type has no `allow`; it cannot be mathematically relaxed by a later plugin).
3. **Discipline mechanized to the extreme**: per-file 100% coverage (treat uncovered lines as a dead-code signal), 28 `verify-*` gates (including forcing every package README to write “limitations” and “model experience”), **`dsh-invariants` depended on by 218/241 packages**, 686 dual-axis-classified Agent Notes (even “do not add an index” has a note).

## Appendix C · Analysis-scope statement

This analysis is based on static reading of `47f9438`, covering: the architectural constitution (full text of `AGENTS.md`), all programmatic documents under `docs/`, line-by-line of the `core/` spine source, the tool pipeline and invariant source, a full census of 241 packages (scripts are reproducible), the test and gate system, the Agent Notes constitution.

**Not covered**: internal implementations of the client 39 packages and typert 11 packages, the concrete implementation of each provider, the per-note content of 686 Agent Notes, the `website/` VitePress projection, the internals of the Python SDK, runtime measurement (did not run `pnpm dsh`). The `extensions` group’s duties and safety boundary for `tool-cordis` / `cordis-host-runner` have been checked (Chapter 29); remaining extension details are still not expanded line by line.

The `self-modification/` in `AGENTS.md`’s layout and `vitest.config.ts`’s coverage exclude of that path are stale entries; take `packages/extensions/` as the authority.

Line numbers are relative to that baseline. This is a high-frequency-evolution repository whose PR numbers have already reached #2519 and whose HEAD is that day’s merge; **when re-checking, take symbol names and file names as the authority**.
