# DeepSeek Harness 源码分析：当「一切皆插件」被贯彻到没有内核

> **分析对象**：[deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)（产品名 **dsh**）  
> **基线**：`v0.1.0-rc.5` @ commit **`47f9438`**（`Merge pull request #2519 from deepseek-harness/feat/npm-public`，2026-08-13）  
> **许可**：MIT（`LICENSE`）· 第三方依赖披露在 `THIRD_PARTY_NOTICES.md`  
> **代码规模**：**497 489 行 TypeScript**（2 319 文件）+ **66 633 行 TSX**（259 文件）+ 4 286 行 Python；**241 个 workspace 包**  
> **服务与工具面**：**73 个 `ctx` 服务键** · **47 个模型可见工具** · **28 个 `verify-*` 门禁** · **686 篇设计决策文档**  
> **产品一句话**：DeepSeek 官方开源的 agent harness，架构是「**everything is a plugin**」——模型适配器、工具注册表、会话日志、**乃至 agent 循环本身**都是插件，全部可从配置替换。  
> **读者对象**：读过本系列任意一份编程 Agent 分析的工程师与架构师。  
> **叙事方式**：五段式「**动机 → 约束 → 被否方案 → 选择 → 代价**」。全文事实标注 `文件:行号`，可回源码核对。  
> **本地基线路径**：`参考项目/deepseek-harness @ 47f9438`  
> **配套教程**：[从零构建插件化 Agent Harness](../从零构建插件化AgentHarness-DeepSeekHarness系-开发全流程教程.md)（20 步造一个 minidsh）

---

## 目录

**Part I · 起点：DeepSeek 为什么要自己造 harness**  
1. [痛点与洞察：模型公司缺的不是模型，是承载模型的骨架](#ch1)  
2. [动机证据：README / AGENTS.md / 论文里的原话](#ch2)  
3. [宏观技术栈：241 个包的数字形状与依赖方向](#ch3)  

**Part II · 宪法：五个观念与三层秩序**  
4. [开发者思维导图：从「一切皆插件」推出全部设计](#ch4)  
5. [Cordis 五观念：空间可替换 × 时间可撤销](#ch5)  
6. [capability seam：三角色缺一不成缝](#ch6)  
7. [三个事件域与四种 dispatch：把「能不能改」写进类型](#ch7)  

**Part III · 主轴：循环、日志、工具**  
8. [turn 与 step：一个循环，两级边界](#ch8)  
9. [inbox 与 `agent/turn-stopping`：不改循环让循环继续](#ch9)  
10. [「模型可见 ⟺ 已记录」：把宪法变成运行时断言](#ch10)  
11. [工具流水线：三道 waterfall + 单调守卫 + 冻结结果](#ch11)  
12. [系统提示与 scope：每步重装配、逐 agent 隔离](#ch12)  

**Part IV · 上下文经济学与任务治理**  
13. [四个服务键管一件事：compaction / spill / pruner / tokenMeter](#ch13)  
14. [goal / plan / todo / schedule / jobs：把「别自由心证」做成插件](#ch14)  
15. [subagent 11 包：从子进程到「别家产品的一轮」](#ch15)  
16. [skill / hooks / mcp：兼容别人的生态](#ch16)  

**Part V · 执行世界与远程面**  
17. [一个执行世界：换两个 provider 搬走五种能力](#ch17)  
18. [沙箱：四平台 `confine(argv)` 与「隔离强度可查询」](#ch18)  
19. [typert：类型图驱动的 RPC，与 11 个包的野心](#ch19)  
20. [四种前端一套行为：CLI / Web / ACP / SDK](#ch20)  

**Part VI · 241 个包全名册与协作图谱**  
21. [名册读法与六大功能域](#ch21)  
22. [域一 · 主轴（30 包）与域二 · 上下文经济（11 包）](#ch22)  
23. [域三 · 执行世界（40 包）与域四 · 任务治理（39 包）](#ch23)  
24. [域五 · 装配与宿主（51 包）与域六 · 前端与支撑（70 包）](#ch24)  
25. [协作图谱：入度、装配链路、一次 turn 的插件参与表](#ch25)  

**Part VII · 工程制度：本系列见过的最强一套**  
26. [逐文件 100% 覆盖率：把覆盖率当删码工具](#ch26)  
27. [28 个 `verify-*`：把口头约定焊成机器门禁](#ch27)  
28. [686 篇 Agent Notes：设计决策考古学的制度化](#ch28)  
29. [动态 Cordis：agent 挂载自己的插件](#ch29)  

**Part VIII · 品味与边界**  
30. [十六项决策五段式复盘](#ch30)  
31. [横向对比：harness 型 vs 循环型](#ch31)  
32. [诚实边界](#ch32)  
33. [源码导览索引与本地复现](#ch33)  

---

<div class="part-band"><span class="band-k">Part I</span>起点：DeepSeek 为什么要自己造 harness</div>

<h2 id="ch1">第 1 章 痛点与洞察：模型公司缺的不是模型，是承载模型的骨架</h2>

### 1.1 先回答「为什么」

一家模型公司开源一个 agent 框架，动机和一家应用公司完全不同。应用公司要的是产品；模型公司要的是**让模型的能力被完整释放的载体**，并且这个载体得能被外界改造成任何形状。

DeepSeek Harness 的 README 只有 55 行，但第一句话就把架构立场说完了（`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*.

三个信息量极大的点：

1. **一切皆插件**——不是"支持插件"，是"**只有**插件"。
2. **底座是 Cordis**，一个独立的、有论文的插件框架，被 vendored 进仓库。
3. 论文标题里的 **spatiotemporal composability（时空可组合性）**——落到工程上就是：空间上服务可按键替换，时间上注册可逆序撤销。

### 1.2 它要解决的具体痛点

本系列分析过的 20 多个 agent 项目，绝大多数是这个形状：一个主循环，加一堆围着主循环长出来的功能。这个形状有一个共同的病：

> **每一个新需求都要回来改主循环。**

权限确认插在工具执行前、上下文压缩插在 while 里、换模型厂商改适配函数、支持第二个前端复制一份循环。三个月后主循环变成一千行泥球，而且**任何外部贡献者都无法在不改它的前提下扩展**。

dsh 的洞察是：**先造一个没有特权内核的运行时，再把循环本身做成插件。**

`docs/architecture.md:13` 那句话是整篇架构文档的胆：

> **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.

「没有特权内核可打补丁」——你扩展 dsh 的方式是**在其它插件旁边挂一个新的**，而不是往中心塞代码。

### 1.3 三个可验证的工程主张

| 主张 | 怎么验证 | 结果 |
|---|---|---|
| **循环本身是插件** | `agent-loop` 是不是一个包？ | 是。`packages/core/agent-loop`，通过 `ctx.agentLoop` 注册（`src/index.ts`），可被配置替换 |
| **配置就是架构** | 能不能打印实际启动的树并替换任意一行？ | 能。`dsh --profile web --dump-config`；文档承诺 "Any row it prints can be replaced by a patch of your own"（`architecture.md:39`） |
| **模型看到的一切可重建** | 有没有运行时断言？ | 有。`ctx.invariants` 服务 + 218 个包的 `./invariant` 伴生插件（`docs/subsystems/invariants.md`） |

### 1.4 与本系列前作的关系

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

同一家公司的两个开源 agent 恰好走了两条路：**Reasonix** 是"单静态二进制 + 缓存优先 + 传输无关控制器"，追求省钱与可移植；**Harness** 是"241 包 monorepo + 一切皆插件"，追求可替换与可扩展。这个对照本身就很有意思——第 31 章会展开。

---

<h2 id="ch2">第 2 章 动机证据：README / AGENTS.md / 论文里的原话</h2>

### 2.1 证据表

| 证据 | 出处 | 它证明了什么 |
|---|---|---|
| 「everything is a plugin」 | `README.md:7` | 架构立场是第一句话，不是特性列表里的一条 |
| 「powered by Cordis … Spatiotemporal Composability」 | `README.md:7` | 底座有独立论文，不是自研玩具 |
| 「There is no privileged core to patch」 | `docs/architecture.md:13` | 没有中心可以塞代码 |
| 「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` | 循环也是插件，写在架构文档正文 |
| 「**Plugins, not loop changes**: changing `agent-loop` requires updating docs/architecture.md」 | `AGENTS.md` | 改循环的门槛被抬到"要改架构文档" |
| 「**Model-visible ⟺ logged**」 | `AGENTS.md` | 可观测性是宪法级约束 |
| 「**Registrations are effects**」 | `AGENTS.md` | 可撤销性是宪法级约束 |
| 「A capability seam comprises Service Definition / Service Provider / Consumer roles. It is complete, never one role」 | `AGENTS.md` | 能力的最小单位是三角色，不是一个类 |
| 「**Trust TypeScript at typed same-process boundaries.** Do not add runtime validation … solely for values the static interface requires」 | `AGENTS.md` | 反过度防御——很少见的规则 |
| 「per-file 100% on `packages/*/*/src`」 | `AGENTS.md` / `docs/testing.md:10` | 覆盖率门禁是逐文件满格 |
| 「**We are DeepSeek — do not ration real-API tests.**」 | `docs/testing.md:19` | 测试策略带着公司身份的自觉 |
| 「Developer preview … **THERE WILL BE COMPATIBILITY-BREAKING CHANGES**」 | `README.md:13` | 大写警告，且宪法有"pre-release stance"专章 |

### 2.2 一条罕见的规则：拒绝过度防御

大多数工程规范教你"多做校验"。dsh 反过来（`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.

它列出了**七个必须校验的边界**（解析器/配置、队列、模型与工具 JSON、持久化/文件、worker、进程、线缆），并明确说其它地方不要加。

理由是逐文件 100% 覆盖率的连锁反应：**你加一个不可能触发的防御分支，就得为它写一个不可能的测试，或者写一条豁免注释**。所以"过度防御"在这套制度下有直接成本。

这是一条我在本系列里没见过第二家写下来的规则。它说明这个仓库的规范不是抄来的最佳实践清单，而是从自己的门禁体系里长出来的。

### 2.3 「pre-release stance」：把技术债策略写进宪法

`AGENTS.md` 有一节标题叫 **"Pre-release stance: foundation over blast radius"**，第一句是：

> **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.

它甚至给自己写了删除条件——**第一个正式 tag 时删掉这一节**。

这种"临时规则带自毁指令"的写法，比"我们以后会重构"的口头承诺有效得多。配套的还有 `SESSION_FORMAT_VERSION` 保持 `0` 且**明确不做兼容承诺**。

---

<h2 id="ch3">第 3 章 宏观技术栈：241 个包的数字形状与依赖方向</h2>

### 3.1 顶层形状

```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 微观技术栈：一次请求穿过的层

宏观说完，看微观。一次模型请求实际穿过的技术层：

```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
```

五层的分工非常干净：**装配层决定有什么，框架层决定怎么连，主轴层决定怎么跑，能力层决定能做什么，前端层决定谁在看。**

### 3.3 依赖入度：真正的重心在哪

我对 241 个包做了一次全量普查（脚本见第 33 章），统计 `package.json` 的包间依赖与 `inject` 声明。入度最高的包：

| 被依赖最多的包 | 次数 | 说明 |
|---|---|---|
| **`dsh-invariants`** | **218** | 241 个包里 **90%** 都依赖它——因为每个包都必须发布一个 `./invariant` 伴生插件 |
| `schemastery` | 110 | vendored 配置校验（`Config` 字段的运行时 schema） |
| `dsh-session` | 84 | 会话日志是真相，谁都要读 |
| `dsh-llm` | 82 | 消息/流式词汇表 |
| `dsh-agent` | 61 | Agent 接口与 inbox |
| `dsh-tools` | 47 | 工具注册表 |
| `dsh-brand` | 30 | **branded id**（宪法要求跨边界 id 不许是裸 `string`） |
| `dsh-system-prompt` | 30 | 提示词注册表 |
| `dsh-timeout` | 23 | 超时策略 |

**`dsh-invariants` 218 次入度**是整份分析里最有信息量的一个数字。它意味着"运行时不变量"不是某个团队的可选实践，而是**包的出生证明的一部分**。第 27 章会讲这套制度怎么运作。

服务侧的入度（`inject` 声明）：

| 被 inject 最多的服务 | 次数 |
|---|---|
| `tools` | 31 |
| `systemPrompt` | 20 |
| `sessions` | 17 |
| `agents` | 13 |
| `subagents` | 12 |
| `llm` | 9 |
| `subprocess` | 8 |

**`tools` 31 次、`systemPrompt` 20 次**说明这两个是最大的汇聚点——半个仓库都在往工具表和提示词里贡献东西。这正是它们必须是**注册表**而不是常量的原因。

### 3.4 技术栈一句话

- **语言**：TypeScript（ESM everywhere，`strict: true` + `noImplicitAny`，每个残留 `any` 必须解释为什么无法收窄）
- **包管理**：pnpm 11.7 workspaces，node `^22.19 || >=24`
- **框架**：vendored Cordis（插件/服务/事件/effect）
- **构建**：tsc 出 lib/types + tsdown 打运行时 bundle；**源码面与产物面严格不混**
- **测试**：vitest × 6 层（单元 / 逐文件 100% 覆盖 / 真实 API e2e / keyless 快照 / 浏览器快照 / 门禁集合）
- **前端**：`apps/web` + `client` 39 包（138 163 行，含 UI slots / primitives / locale / connection 等分层）
- **原生**：Landlock 启动器是 **C11** 静态可执行文件（`native/landlock-run/packages/entry/src/main.c`），不是 N-API addon；npm 名带 `node-addon` 只是历史命名。Linux 上 `sandbox-local` 先试 **bwrap** 再 fallback Landlock；darwin 用 **Seatbelt/`sandbox-exec`**；win32 用 **windows-acl** 受限 token（`sandbox-local/src/index.ts` 的 `PLATFORM_CHAINS`）
- **协议面**：ACP（Agent Client Protocol）、JSON-RPC SDK（TS + Python）、Typert RPC 网关

---

<div class="part-band"><span class="band-k">Part II</span>宪法：五个观念与三层秩序</div>

<h2 id="ch4">第 4 章 开发者思维导图：从「一切皆插件」推出全部设计</h2>

整个仓库的设计可以从一句话推导出来。这张图是全文的骨架：

```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 行生成文档
```

七个分支，每一个都是从"没有中心"这个约束**倒推**出来的必要条件：

- 没有中心 → 使用者不能 import 实现 → **需要服务仓库**
- 服务可替换 → 需要接口与实现分离 → **需要 seam 三角色**
- 插件可来去 → 注册必须可撤销 → **需要 effect**
- 装配决定行为 → 装配必须可检查可覆盖 → **需要分层 patch + dump-config**
- 没有中心持有状态 → 状态必须在一个共享真相里 → **需要 append-only 日志**
- 任何人都能挂 listener → 安全边界不能靠 listener → **需要单调守卫**
- 规则太多且协作者含 AI → 口头约定必然腐化 → **需要机器门禁**

---

<h2 id="ch5">第 5 章 Cordis 五观念：空间可替换 × 时间可撤销</h2>

### 5.1 五句话

`docs/cordis-primer.md:9-13` 用五句话定义了整个框架：

| # | 观念 | 原文关键句 |
|---|---|---|
| 1 | **插件是实现 Service 的对象** | "a function with optional `inject` and `apply(ctx)` fields, or a `Service` subclass whose lifecycle Cordis mounts into the current context" |
| 2 | **context 是服务仓库** | "other plugins find services **via key instead of importing a concrete implementation**" |
| 3 | **用 `inject` 声明服务依赖** | "**load order is expressed through service requirements rather than manual boot sequencing**" |
| 4 | **Typed Events 通信** | "declare event names through TypeScript declaration merging, then dispatch them as `emit`, `waterfall`, `parallel`, or `serial`" |
| 5 | **注册是可逆 effect** | "installed through `ctx.effect()` or `ctx.on()` so reload and teardown unwind them predictably" |

第 3 条是整个架构的转折点。**"加载顺序通过服务需求表达，而不是手工启动排序"**——这一句消灭了 agent 框架里最恶心的一类复杂度：启动顺序表。

### 5.2 服务键怎么声明：declaration merging

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

这一招同时拿到两件通常互斥的东西：

- **全局类型可见**：任何包里写 `ctx.shell.run(...)` 都有完整类型提示
- **实现运行时注入**：没有 import 任何具体实现

glossary 对 Service Definition 有一条严格约束（`docs/glossary.md:9`）：它必须是 Cordis 的 `Service`（抽象类如 `ShellExecutor`，或具体注册表如 `WebRuntime`），**"never a TypeScript `interface`"**。

为什么不能是 interface？因为 interface 只有类型没有运行时存在，而 seam 需要一个能被 `provide` 到 `ctx` 上、能持有生命周期、能被 HMR 拆掉的**运行时实体**。

### 5.3 服务重名怎么办：fail loud

`packages/shell/shell/src/index.ts:16-20` 的注释解释了 shell 设置命名空间为什么归 Definition 而不归任何一个 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.

三个收益一次拿到：Windows 层通过**替换配置行**而不是加分支来切 PowerShell；同时挂两个 provider 会**立刻炸**而不是静默取一个；跨平台带过来的设置文档在两边都能解析。

### 5.4 四种 dispatch 与 waterfall 语义

见第 7 章。

### 5.5 代价

| 代价 | 具体表现 |
|---|---|
| 学习曲线陡 | 读懂一次请求要同时理解服务仓库、inject 解析、四种 dispatch、effect 生命周期 |
| 间接层多 | 想知道 `ctx.shell` 到底是谁，得看装配配置而不是看代码 |
| 调试需要新工具 | 所以才有 `--dump-config`、`cordis_inspect_*` 工具、`docs/module-graph.md` |
| vendored 依赖要维护 | `vendor/README.md` 有 manifest + 同步流程 + 已记录的本地修改，每次同步要重跑 test 和 build |

第 4 条值得注意：dsh 把 Cordis **vendored** 进仓库而不是当 npm 依赖。`AGENTS.md` 的 vendoring policy 规定：pinned 源码副本、manifest 带上游 SHA、通过同步流程更新、**本地修改要重新应用或退役**、同步后必须重跑 `test && build`。

这是一个务实的选择：框架和产品在同一个演进节奏上，还没到能锁版本的阶段。

---

<h2 id="ch6">第 6 章 capability seam：三角色缺一不成缝</h2>

### 6.1 动机与约束

**动机**：让"执行 bash"这类能力可以整体替换（本机 / 沙箱 / 远程 / PowerShell），而**模型看到的工具契约保持不变**。

**约束**：模型看到的 schema 一旦变化，提示词缓存失效、模型行为漂移、快照测试全红。所以**替换必须发生在模型看不见的层**。

### 6.2 被否方案

| 被否 | 为什么否 |
|---|---|
| 工具内部 if/else 选实现 | 每加一种实现改一次工具；工具变成实现的目录 |
| 用 TypeScript `interface` 当接口 | 没有运行时实体，无法 provide、无法持有生命周期、无法 HMR |
| 一个包同时是接口+实现+工具 | 换实现要 fork 整个包，连工具一起 fork |

### 6.3 选择：三角色

`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` 把它升级成硬规则：

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

shell 是官方指定的范本（`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 一个精妙细节：request/spec 分裂

`AGENTS.md` 指定 shell 为"显式优于隐式"的模板：

> **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`（字段可选）与 `ShellExecSpec`（字段齐全）是两个类型。默认值解析是一个**显式的、可被记录和断言的步骤**，而不是藏在 `run()` 里的 `??`。

不这么做的代价很具体：调用方不知道实际超时是多少、日志里查不到、换 provider 后默默变了、测试断言不了。

### 6.5 Definition 的边界感

`packages/shell/shell/src/index.ts:1-5` 的模块 JSDoc：

> 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.**

**"让执行器与会话无关"**——后台作业的 id、归属、轮询、通知全部归 `ctx.jobs`。所以同一个执行器能同时服务聊天、自动化协议、子 agent 三种场景。

这种"把不属于我的东西推出去"的边界感，在 241 个包的模块 JSDoc 里反复出现。它是这套架构能长这么大还没塌的原因。

### 6.6 代价

- **一个能力至少三个包**。仓库包数因此膨胀到 241。
- **导航成本**：想改"bash 超时"要先判断该改 Definition（契约）、Provider（实现）还是 Consumer（schema）。
- **过度拆分的风险**：宪法明写 "split only when roles evolve independently"（角色独立演进时才拆），并允许一个包持有多个角色（`dsh-llm` 同时是 Definition 和 Consumer）。

---

<h2 id="ch7">第 7 章 三个事件域与四种 dispatch：把「能不能改」写进类型</h2>

### 7.1 三个事件域

`docs/architecture.md:55` 说得很直白：**"Events are the extension points, and picking the right domain is the first decision in most changes."**

| 域 | 语义 | 什么时候用 |
|---|---|---|
| **Session events** | 追加到日志的**持久事实**，通过 `session/event` 广播 | 这个事实必须活过重载 |
| **Agent events**（`agent/*`） | 携带活体 `Agent`：inbox / step / status / request / validation / continuation | 观察或**拦截**在飞的工作 |
| **Capability events**（`fs/*`, `tools/*`, `telemetry/*`） | 把策略与适配器挂到 seam 上 | 加策略而**不 import 循环** |

三域的完整生产者/消费者表在 `docs/event-producer-consumer.md`（生成的）。

### 7.2 四种 dispatch 模式

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

| 模式 | await？ | 顺序 | 有返回值？ | 用途 |
|---|---|---|---|---|
| `emit` | 否 | 注册序 | 否 | 观察 |
| `waterfall` | 否 | 注册序 | **是** | 改写、拦截、策略 |
| `parallel` | 是 | 并行 | 否 | 扇出 |
| `serial` | 是 | 注册序 | 是 | 依次表态 |

关键的一句（`: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.

**dispatch 模式是公共契约**，用 `@mode` 标签写在 JSDoc 里，**生成的目录会检查声明与实际 dispatch 点是否一致**。

### 7.3 技术闪光点：把「能改」和「只能看」用类型分开

看工具注册表的四个事件（`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
```

前三个是 waterfall（**能改写**），最后一个是 emit + 两个参数都 `Readonly<>`（**只能观察**）。

而且它不止靠类型——还有运行时不变量（`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')
```

**结果一旦定稿就冻结，任何观察者都改不动，而且违反会被运行时抓住。**

这是我在本系列里见过的对"事件系统"最讲究的处理。多数项目的 EventEmitter 里，"某个 listener 偷偷改了已定稿的结果"是一类极难调试的 bug；dsh 用 dispatch 模式 + `Readonly` + `Object.freeze` + 运行时断言四重设防把它变成不可能。

### 7.4 waterfall 是 around 中间件

`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.**

配合 `AGENTS.md` 的大写规则：

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

短路**是设计的一部分**（`: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.

这条规则的危险性在于**违反它不报错**：一个只想记日志的 listener 忘了 `next()`，下游所有策略静默失效，而日志一切正常。所以它被顶到 `AGENTS.md` 并用大写 MUST。

---

<div class="part-band"><span class="band-k">Part III</span>主轴：循环、日志、工具</div>

<h2 id="ch8">第 8 章 turn 与 step：一个循环，两级边界</h2>

### 8.1 定义

`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**.

两个刻意的措辞：

- **"zero or more steps"**——零 step 的 turn 合法（见 8.4）
- **"once nothing is owed"**——结束条件是"不欠了"，而不是"模型说完了"

### 8.2 完整流水线

架构文档给了一段伪代码（`:67-82`），我把它对照源码画成时序图：

```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）
```

源码位置：外层 `while (await this.turn()) {}` 在 `packages/core/agent-loop/src/agent.ts:212`，`turn()` 在 `:246`，`step()` 在 `:332`，`buildRequest()` 在 `:407`。

### 8.3 四个技术闪光点

**① `max-tokens` 是 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**.

一个 turn 里第 3 步撞了输出上限、第 4 步正常结束——turn 的结论必须是"撞了上限"。因为下游（UI 提示、重试策略、评测）要据此判断结果是否可信。**降级就等于撒谎。**

**② 每个 chunk 都进日志，且消息回指其来源**（`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` 让任何一条最终消息可以回溯到拼成它的原始流式分片。调"为什么这条回答被截断"这类问题时这是救命的。

**③ 请求错误是可重试的 waterfall，而不是 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）
```

**重试策略是插件**，不是循环里的常量。而且重试会重新走 `buildRequest`——所以重试时可以换 provider/模型。

**④ 失败是结构化的**（`agent.ts:307-314`）：

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

`turn/end` 在 `finally` 里必发（`:316-322`）。**永远不要让日志里出现半个 turn**——否则重放逻辑要处理不完整状态，那是无底洞。

### 8.4 零 step 的 turn 为什么必须存在

```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
}
```

用户按了发送，但某个插件把消息拦空了。这时仍然开一个 turn 并正常关掉。architecture.md 解释（`:88`）：

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

不这么做的后果：UI 上出现"我明明发了消息但什么都没发生"，而日志里查不到任何痕迹。**"用户发起过一次尝试"本身是需要被记录的事实。**

### 8.5 一个制度痕迹：`/* 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`)
```

这是**逐文件 100% 覆盖率门禁**留下的痕迹：不可达的防御分支必须显式豁免**并写清理由**。而 `scripts/coverage-exempt.ts` + `coverage-exempt.spec.ts` 又在守着这些豁免本身。见第 26 章。

---

<h2 id="ch9">第 9 章 inbox 与 `agent/turn-stopping`：不改循环让循环继续</h2>

### 9.1 一个 inbox，两个投递口

`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.**

四种输入来源，两种唤醒语义（`packages/core/agent-loop/src/agent.ts:122-132`）：

| API / 来源 | 唤醒？ | 投递口 |
|---|---|---|
| `followup` / 用户消息 | 立刻 | **next-turn** |
| 工具产生的附加上下文 | 不需要（当前 turn 内） | **next-step** |
| `steer` | 立刻 | **next-step** |
| `inject` | **不唤醒** | **next-step**（等当前或下一次已唤醒的 step 边界领取） |

`inject` 与 `steer` 进同一条 `next-step` 队列，差别只在 wake。**注入不主动开 turn**：idle 时只进 inbox；running 时在「已 claim 的 batch 之后」的 step 边界才会被吃到。否则每注入一句「当前分支是 main」就空开一轮。

Inbox 不是纯内存队列。先 `session.append('agent/inbox/spliced', …)`，再改 live 投影（`packages/core/agent/src/inbox.ts:157-193`）。cancel / resume / fork 都能从日志重建 pending work。

循环里 `target` 的切换（`agent.ts:261,300`）：

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

### 9.2 工具怎么往 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
```

两个细节：

- 第六个参数是投递回调——这是 `tools/post-execute` 的 "add context" 决策落地处。流水线文档描述为 "Active-batch additionalContexts FIFO → injected user/message **after recorded tool results**"。
- `concluded ? completed : null`——**"还欠不欠一次请求"由工具批次自己报告**，不是循环猜的。

### 9.3 技术闪光点：`agent/turn-stopping` 的"公告—异议"模式

循环要停之前的两次检查（`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                 // 再查一次
```

发完 serial 事件**再查一次 inbox**。因为 listener 完全可以在这一刻往 inbox 里塞新东西。

这是"**不改循环让循环继续**"的标准答案：给循环一个"我要停了"的公告时刻，任何插件都能在这一刻提出异议。

`agent/turn-stopping` 是 **serial 且没有 `next()`**（`architecture.md:84`）——所有 listener 都会被依次 await，谁都不能短路掉别人的异议权。Listener 的标准异议方式是 **`steer`**（往 `next-step` 塞消息并 wake），然后二次检查 inbox 非空，turn 继续。这是「不改循环让循环继续」在**同一个 turn 内**的答案。

同 session 的 **goal 续跑不是这条钩子**。`goal-round-driver` 听 `agent/status` 进入 idle、goal 仍 armed，再 `agent.followup` 开**下一个 turn**（见第 14 章）。把 goal 续跑写进 `turn-stopping` 是错的：那会把「这一轮还欠一步」和「目标还没做完、另开一轮」混成一件事。

### 9.4 wake latch：拆卸不等模型轮次

`agent.ts:164-193` 的 `wakeDriver`：

```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` 是三态：`idle` / `maintenance` / `running`（`agent.ts:38-46`）。**maintenance 阶段**是别家少见的设计——驱动在做维护工作（如压缩、标题生成）时不接受唤醒投递，而是 latch 住等收敛。

**而 disposal 从不 latch**：`so teardown waits on no model turn`（拆卸不等任何模型轮次）。这类"拆卸不能被业务逻辑拖住"的细节，是长期跑在生产里的 harness 和 demo 的分水岭。

---

<h2 id="ch10">第 10 章 「模型可见 ⟺ 已记录」：把宪法变成运行时断言</h2>

### 10.1 动机

第 0 步那种 `const messages = [...]` 的方案里，模型输入活在内存。于是断线重连、会话分叉、审计"模型到底看到了什么"、UI 重放流式过程、评测复现——全都做不到。

### 10.2 选择：日志是真相，消息是投影

`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.**

方向很关键：不是"消息数组顺便记日志"，而是"**日志是真相，消息数组是投影**"。

`packages/core/session` 的模块 JSDoc：

> 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`).

**持久化是插件关心的事**——会话服务本身不碰磁盘。所以 session 组有 13 个包：日志核心 + JSONL/zstd 持久化 + 投影 + 标题生成 + 遥测 + 引用解析…

### 10.3 技术闪光点：不变量制度化

`docs/architecture.md:96` 与 `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.

具体断言挂在 `llm/stream` 入口（`packages/core/agent-loop/src/invariant.ts:21-52`）：对 agent-loop 发出的请求，把 `GenerateOptions` 冻结后，要求 `session.deriveMessages()` 与 `options.messages` 的 JSON 全等，否则 fail（`log-reconstruction desync`）。`agent/request` waterfall **不能改 messages**；要改历史只能 append session 事件。不这么做：插件可以 silently 改发出去的数组，live 与 replay 分叉且无断言。

不变量不是一个全局 assert 函数，而是**逐包所有权的注册服务**（`docs/subsystems/invariants.md`）：

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

制度要点：

| 机制 | 细节 |
|---|---|
| **每包一个伴生插件** | 每个 workspace 包发布 `./invariant`，用**自己的完整 npm 包名**注册 |
| **违规可归属** | 抛 `InvariantError`，稳定 `code: 'INVARIANT'` + `packageName`，消息前缀 `invariant violated by "<package>": …`——**注册表不 import 任何产品包** |
| **子 fiber 隔离** | installer 在专属 Cordis 子 fiber 里跑，`installer.inject` 声明该 fiber 能访问哪些服务 |
| **名字预留** | 即使过滤器关闭了检查，包名仍被预留——**两个插件永不可能静默抢同一个名字** |
| **原子失败** | installer 失败则子 fiber 被 dispose 且预留被释放，两者原子 |
| **配置 fail loud** | 正则白/黑名单，黑名单胜过白名单；空串/带空格/重复/无效正则在服务启动时**抛错而不是跳过** |

最讲究的是**它规定了检查可以断言什么**（`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.**

不许写"检查 `ctx.shell` 是否存在"这种**假装在检查**的断言。没有可断言的真实关系时，正确做法是导出空 installer，**并用以 `No runtime invariant:` 开头的注释解释为什么**。

而且这条也被机器守着：`pnpm run verify-package-invariants` 拒绝「生成的占位标记」「没解释的空 installer」「非空 installer 却忽略 reporter」「注册名写错」「导出/发布/依赖/bundle 接线不全」。

**这就是 `dsh-invariants` 被 218 个包依赖的原因。** 本系列 21 个项目里没有第二家这么做。

### 10.4 会话日志版本机制的一个精妙设计

`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`.

默认严格：旧版本程序读到不认识的事件类型就**拒绝整个日志**，而不是跳过它继续。因为跳过意味着"模型可见的历史缺了一块"，而那会产生静默错误的重放。

想让某个事件可被旧版本忽略，必须**显式**在信封上标 `ignorable: true`。**默认安全，例外显式。**

### 10.5 代价

- 任何"给模型加点上下文"的需求都要先扩 `SessionEventMap` + 加渲染，比 push 一条消息麻烦得多
- 日志体积大（每个 chunk 都记）——所以有 JSONL + zstd 持久化和 packed-row 布局
- 218 个包的 invariant 伴生是真实的维护负担（虽然大部分是带解释的空 installer）

---

<h2 id="ch11">第 11 章 工具流水线：三道 waterfall + 单调守卫 + 冻结结果</h2>

### 11.1 动机

工具执行是 agent 里最需要插拔的地方：权限、审批、沙箱、超时、重试、指标、结果改写、UI 渲染、文件读写策略、hook 桥接…全塞进 `runTool()` 就是千行泥球。

`docs/tool-execution-pipeline.md` 的第一句话点明设计目标：

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

### 11.2 完整流水线

`packages/core/tools` 的模块 JSDoc 概括为五段：

> 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 技术闪光点一：单调守卫

流水线里有一处看起来多余的设计：既然有 `tools/pre-execute` waterfall，为什么还要额外一层守卫？

因为 waterfall 有致命的灵活性：**后注册的 listener 能覆盖前面的决定**。

```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
})
```

于是 dsh 在可扩展的 waterfall **之后**再跑一层守卫。流水线文档的描述：

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

**只有两个选项：拒，或者不表态。没有"允许"这个返回值**，所以它在数学上不可能放宽任何东西。源码 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` …

**"after the extensible"** 这个措辞很精确：可扩展的部分先跑，不可放宽的部分后跑。**顺序本身就是安全设计。**

| | waterfall | 单调守卫 |
|---|---|---|
| 能改写别人的决定 | 能 | **不能** |
| 顺序敏感 | 是（后者赢） | 否（任一 deny 即终局） |
| 新插件能不小心提权 | **能** | **不可能** |
| 适合 | 策略协作、上下文补充 | **安全边界** |

### 11.4 技术闪光点二：流水线顺序本身是运行时契约

`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')
}
```

**阶段顺序不是靠代码结构隐含保证，而是被运行时不变量显式断言。** 一个写错的插件如果绕过了阶段顺序，会立刻炸并归属到它的包名。

### 11.5 技术闪光点三：UI 渲染意图是设计的一部分

`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`**.

工具在定义时就要说清"我该被渲染成普通卡片、终端输出、还是 diff"。而且渲染函数必须是 `args` 的**纯函数**——所以待处理卡片能在**执行之前**就画出来（流水线图里 `presentCall(args)` 在 `tools/pre-execute` 之前）。

### 11.6 47 个工具的目录是 boot 出来的

`docs/tool-catalog.md`（1 873 行）文件头注释：

> 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.**

工具 schema 不是静态可知的（运行时展开的枚举、拼接的描述、配置驱动的名字、MCP 的原始 JSON-Schema），所以生成器**真的启动每个工具插件**去读 schema。而且有完整性守卫：**新工具不可能被静默地漏掉文档**。

47 个模型可见工具（我的普查结果）：

```
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
```

最后那一行是第 29 章的主角：**agent 可以定义、运行、检查自己的 Cordis 插件**。

---

<h2 id="ch12">第 12 章 系统提示与 scope：每步重装配、逐 agent 隔离</h2>

### 12.1 systemPrompt 是注册表，不是模板

`packages/core/system-prompt`（1 582 行）的模块 JSDoc：

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

四类贡献。被 30 个包依赖、`inject` 入度 20——**半个仓库都要往提示词里塞东西**，这正是它必须是注册表的原因。

装配发生在 **每个 step**（`agent.ts:230`）：

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

不是每个 turn 一次。因为工具集可能在 turn 中途变化：子 agent 起来了、计划模式退出了、`cordis_run` 挂载了新工具（第 29 章）。

`assembleContextFor(this, signal)` 把"哪个 agent 在问"和取消信号一起传进去——**装配是 per-agent 的**。

### 12.2 scope：两级、扁平、不继承

`packages/core/scope`（1 215 行）的模块 JSDoc：

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

glossary 的定义更严格（`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.**

三个刻意的限制：

1. **只有两级**（全局 / 单个 scope），没有多级嵌套
2. **不向下继承**——子 agent 不自动获得父 agent 的 scoped 注册
3. 需要子树行为时用 **lineage 数据**表达，**绝不用 scope 结构**

为什么这么克制？因为"作用域继承"是权限系统里最容易出错的地方。一旦允许继承，就要回答"子 agent 能不能覆盖父的限制""孙子怎么办""fork 之后呢"。两级扁平把这些问题全部消灭。

一个实例（`docs/tool-catalog.md` 的 `dsh-tool-subagent-report`）：

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

`report` 工具只在可续的进程内子 agent 里可见，而且**能穿过全局工具过滤器**——因为它是子 agent 向父汇报的唯一通道，不能被过滤掉。

---

<div class="part-band"><span class="band-k">Part IV</span>上下文经济学与任务治理</div>

<h2 id="ch13">第 13 章 四个服务键管一件事：compaction / spill / pruner / tokenMeter</h2>

上下文管理在多数项目里是一个函数。dsh 拆成了**四个独立的服务键**，因为它们解决的是四个不同的问题。

### 13.1 问题分解

| 服务键 | 问题 | 包 |
|---|---|---|
| `ctx.compaction` | **对话太长**：20 轮后历史超窗口 | `packages/compaction`（4 包 · 8 032 行） |
| `ctx.spillStore` | **单次输出太大**：grep 匹配 3 万行 | `packages/spill`（3 包 · 1 473 行） |
| `ctx.toolResultPruner` | **工具结果需要修剪**：保留结构、删冗余 | 在 core/tools 侧 |
| `ctx.tokenMeter` | **要知道花了多少**：计量与预算 | `docs/subsystems/token-meter.md` |

只做第一个（压缩历史）的项目，遇到第二种情况会直接崩窗口。只做第二个（截断输出）的项目，模型永远看不到被截掉的部分。

### 13.2 spill：落盘 + 定位符 + 取回说明

`packages/spill` 的 Definition JSDoc（我的普查抓到的原文）：

> 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**.

三个词是关键：**persist（落盘）+ locator（面向模型的定位符）+ retrieval guidance（怎么取回的说明）**。

不是简单截断，而是完整内容存起来、给模型一条"我把完整结果存在这里，你可以这样读"的消息。

真实用法（`docs/tool-catalog.md` 的 `dsh-tool-fs-search`）：

> 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**.

**溢出的内容不是坟墓**——定位符指向的东西还能被 `read` 和 `grep` 继续处理。

### 13.3 compaction 的难点不是摘要，是"算不算模型可见"

按第 10 章的宪法：压缩结果**算**模型可见，所以它必须是一个 session event，不能是内存里的变量。

这带来三个收益：压缩**可审计**（看得到压了哪段、摘要是什么）、**可重放**（重放日志得到同样的模型输入）、**可撤销**（删掉那个事件就恢复原状）。

代价是压缩不能"就地改数组"，实现复杂度更高。

### 13.4 `ctx.spillStore` 是 optional 的

注意上面那句 "the **optional** `ctx.spillStore` backend"。工具在没有 spill 后端时仍然工作（只是截断），有后端时自动升级为"截断 + 定位符"。

这是 seam 设计的一个次级收益：**能力缺失是一种合法的部署形态**，而不是启动失败。对比之下 `ctx.lsp` 的处理是（`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.**

**没有 provider 时返回结构化错误，而不是改变 schema。** 因为改 schema 会让提示词缓存失效、让模型行为漂移。**模型看到的契约必须与部署细节解耦。**

---

<h2 id="ch14">第 14 章 goal / plan / todo / schedule / jobs：把「别自由心证」做成插件</h2>

### 14.1 goal：event-sourced + CAS + 人类根权限 + 有界

`packages/goal`（4 包 · 5 786 行 · `ctx.goals`）的模块 JSDoc：

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

工具目录里的部署说明（`docs/tool-catalog.md` 的 `dsh-tool-goal`）信息量极大：

> `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.**

三个设计逐个拆：

**① 创建/编辑/暂停/恢复需要"直接人类根权限"。** 模型不能给自己派目标，也不能悄悄改目标。否则"把测试跑绿"会被改成"把测试注释掉"。

**② `complete` / `blocked` 要带上确切的当前轮次。** 这是 compare-and-set：模型说"我完成了第 5 轮的目标"，若实际已是第 6 轮，声明失效。**防的是过期声明。**

**③ `blocked` 有下界：至少 3 个已准入轮次。** 模型不能第一轮就说"我卡住了"，必须真的试过三轮。

续跑不走 `agent/turn-stopping`。`goal-round-driver`（`packages/goal/goal-round-driver/src/index.ts:137-205`）在 agent **idle**、goal **active 且 armed**、inbox 无竞争工作时，渲染 `<goal_round>` 并 `agent.followup`。Resume 后 durable phase 仍在，但 **activation 是进程内态、默认 disarmed**，避免冷启动自动续跑。循环代码一行未改：driver 只消费公开的 `agents` / `goals` / `sessions`。

### 14.2 plan：把计划模式做成被日志记录的状态

`AGENTS.md` 的仓库布局里，plan 的描述是 **"plan mode as logged state"**。

对比 Claude Code 的 plan mode（一个会话级布尔开关 + 提示词变化），dsh 的做法是 `ctx.planMode` 服务 + 日志事件。工具目录里 `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**.

三个讲究之处：

- **工具在非计划模式下也留在 schema 里**——因为进出计划模式不该改变模型看到的工具表（缓存友好 + 快照稳定）
- 执行路径**拒绝**模式外的调用——schema 稳定不等于行为放宽
- 批准后**在 step 边界**记录"计划模式失效"——状态变更有明确的日志时刻

### 14.3 schedule：版本化的定时能力

`docs/tool-catalog.md` 的 `dsh-schedule`：

> 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.

"**Version 1**"这个措辞值得注意——它把"这个工具的能力集"当作一个有版本的契约，而不是随意增长的参数表。`every_seconds` 是**有界的**（不能设成每毫秒一次）。而且**明确披露**投递是 session-local 的（不跨会话）。

### 14.4 jobs：kind-agnostic 的后台作业控制器

`docs/tool-catalog.md` 的 `dsh-tool-jobs`：

> 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()`**.

**三个工具（`job_list` / `job_output` / `job_kill`）管所有种类的后台工作。** 后台 bash、PTY 发送、子 agent 全部走同一套。

这解决了一个真实的模型体验问题：如果每种后台工作有自己的一套管理工具，模型要记 9 个工具名而不是 3 个，而且经常用错。

而且 `ctx.jobs` 的设计让**执行器与会话解耦**（回看第 6.5 节 shell Definition 的 JSDoc）——作业 id、归属、轮询、通知归 jobs，执行器只管执行。

### 14.5 todo：allowParallelInProgress 是必填无默认

`docs/tool-catalog.md` 的 `dsh-tool-todo`：

> `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.

**必填无默认**是刻意的。因为"能不能同时有多个进行中任务"是一个**产品语义选择**，不该有隐含默认。而且这个选择会改变**工具描述文本**——同一个工具，两种教模型的方式。

这正是 `AGENTS.md` 那条 "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：循环卫生

`packages/guard` 只有两个子包，没有统一入口包：`repeat-tool-reminder`（同一 tool+canonical args 连续 hammer 时注入提醒，**从不 veto**）和 `timeout-policy`（在 `tools/execute` 包一层 deadline，工具声明了 `timeoutMs` 却不 honor signal 时给出结构化 `TOOL_TIMEOUT`）。`AGENTS.md` 写 "loop-hygiene + tool-timeout plugins"。max-steps / token budget 不在这个组里。

---

<h2 id="ch15">第 15 章 subagent 11 包：从子进程到「别家产品的一轮」</h2>

`packages/subagent` 是除 client 和 core 外最大的组：**11 包 · 24 466 行**。

### 15.1 provider 的变化范围有多大

`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**.

一个接口后面，从"新起一个子 agent"到"**委派给另一个产品的一轮对话**"。普查里能看到 `subagent-codex`、`subagent-claude-code` 这样的包名——**它把 Codex 和 Claude Code 当作 subagent provider**。

这与本系列 Open Design（把别人的 CLI 当引擎）、DeepTutor（把编程 CLI 当可会诊的知识库）是同一个思路的第三种形态：**dsh 把它们当作 subagent seam 的 provider**，因此它们自动获得了 jobs 管理、session 记录、工具流水线的全部基础设施。

### 15.2 三层工具设计

工具目录里 subagent 相关有四个包，分工很清楚：

| 包 | 工具 | 设计要点 |
|---|---|---|
| `tool-subagent` | `subagent` / `subagent_fork` | **注册名是 load-time config**（`toolName`），所以同一个包被加载多次，每个 backend 一个工具名 |
| `tool-subagent-control` | `send_message` `interrupt_agent` `list_agents` | **全局注册一次**的控制工具，管所有可续的后台子 agent |
| `tool-subagent-report` | `report` | **per-child 注册**，只在子 agent 内可见，且穿过全局 toolFilter |

`tool-subagent` 那条部署说明特别能体现"配置即架构"：

> 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.

**同一个包加载两次，产出两个语义不同的工具**（一个可续+默认后台，一个一次性+默认前台）。这是插件化架构才能做到的事——在 `main()` 型架构里你得写两个工具类。

### 15.3 `ralph`：一个有意思的工作流工具

`docs/tool-catalog.md` 的 `dsh-tool-ralph`：

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

每轮起一个全新的结构化子 agent，模型只能选"不可变的目标"和轮次上限。**模型不能改目标，也不能改流程**——这是"把确定性骨架沉进工具"的又一个实例（对照 DeepTutor 第 14 章的同类设计）。

---

<h2 id="ch16">第 16 章 skill / hooks / mcp：兼容别人的生态</h2>

### 16.1 hooks：桥接 Claude Code 与 Codex 的 hook 协议

`AGENTS.md` 的仓库布局里：

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

`packages/hooks`（3 包 · 4 933 行）**实现了别家产品的 hook 线缆协议**。这意味着为 Claude Code 写的 hook 脚本可以在 dsh 里跑。

这是一个很有战略意味的决定：**降低迁移成本**。用户已有的 hook 投资不作废。而且它被做成了独立的 wire-protocol 库，而不是硬编码在工具流水线里。

在流水线图里 hooks 挂在 `tools/pre-execute`（"hooks · permission · sandbox"）并产生自己的 session 事件（`hook/invoked` / `hook/result`）——**hook 的调用与结果也是日志可见的**。

### 16.2 skill：注册表 + 本地实现 + catalog/loader 工具

`packages/skill`（4 包 · 6 242 行）：provider 注册表 + 本地实现 + catalog/loader 工具。

工具目录里 `dsh-tool-skill` 的一行很值得注意：

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

技能目录是通过 **`agent.inject()` 替换 user/message 目录**来更新的——回看第 9 章，`inject` 不唤醒循环，所以技能目录的刷新不会触发额外的模型调用，而是等下次有真实消息时一起进去。**这是 inbox 双语义设计的一个漂亮应用。**

### 16.3 mcp：一个包 3 322 行

`packages/mcp` 只有 1 个包。MCP 工具的 schema 是**原始 JSON-Schema**（见 `tool-catalog.md` 生成器那段解释），所以它们无法被静态分析，必须 boot 后读取。

---

<div class="part-band"><span class="band-k">Part V</span>执行世界与远程面</div>

<h2 id="ch17">第 17 章 一个执行世界：换两个 provider 搬走五种能力</h2>

### 17.1 架构最强的卖点

`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 论断的代码依据

关键在**依赖方向**：没有任何上层能力直接 import `node:child_process`。我在普查里验证了这点——`subprocess` 被 8 个包 inject。

最能说明问题的是搜索工具（`docs/tool-catalog.md` 的 `dsh-tool-fs-search`）：

> 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"**：搜索不经过 shell。所以搜索能力不依赖"机器上有没有 bash"，能在远程沙箱、Windows、精简容器里一致工作。

### 17.3 e2b 是这个论断的实证

`packages/e2b`（3 包 · 6 630 行），`AGENTS.md` 的定位：

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

**一个 POC 用 6 630 行证明架构承诺：换两个 provider，五种能力跟着搬家。**

执行世界各组体量（普查数据）：

| 组 | 包 | 行数 | 角色 |
|---|---|---|---|
| `fs` | 7 | 13 044 | 文件系统缝 + 策略（含 read-before-write 策略插件） |
| `shell` | 9 | 10 405 | bash/pwsh 缝 + 4 provider + 3 consumer |
| `sandbox` | 4 | 9 040 | 沙箱缝 + 策略 |
| `e2b` | 3 | 6 630 | 远程实证 |
| `terminal` | 3 | 5 721 | 持久 PTY |
| `lsp` | 3 | 5 496 | 语言服务 |
| `subprocess` | 2 | 4 256 | 进程树 |
| `code-runtime` | 2 | 3 963 | 代码模式运行时 |
| **合计** | **33** | **58 555** | **全部建在两个原语 seam 上** |

### 17.4 code-runtime：Code Mode

`docs/tool-catalog.md` 里 `run_code` 那条是全文最长的部署说明，值得完整理解：

> 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.

拆开看：

1. **Code Mode 下 `run_code` 是注册表唯一的线缆贡献**——模型只看到一个工具
2. 其它能力以**生成的 SDK 段**出现（用运行时的语言，比如 Python/JS）
3. 程序通过 bindings 调用它们，**重新进入完整的受守卫工具流水线**
4. 每个嵌套执行**链接到外层结果**
5. 并发有原生契约：提交序启动、并发安全体最多重叠 `maxParallelSubCalls`

**这是"工具调用"到"写程序调用工具"的形态跃迁**，而且没有绕过任何安全检查——嵌套调用重新走一遍守卫流水线。本系列里 Claude Code 的 code execution、Reasonix 的 use_capability 都做过类似探索，dsh 的版本在"嵌套执行也受完整流水线约束"这点上最彻底。

---

<h2 id="ch18">第 18 章 沙箱：四平台 `confine(argv)` 与「隔离强度可查询」</h2>

### 18.1 沙箱不执行，只包装 argv

`docs/architecture.md` 的扩展点表：

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

这个设计让沙箱能与任何 provider 组合：本机 bash、远程 subprocess、LSP 子进程，谁都能在 spawn 前包一层。**如果沙箱自己负责执行，就会和执行器争夺同一个职责。**

### 18.2 平台链不是「只有 Landlock」

`sandbox-local` 的 `PLATFORM_CHAINS`（`packages/sandbox/sandbox-local/src/index.ts:159-165`）：

| 平台 | 机制 |
|---|---|
| Linux | **bwrap 优先**，失败再 Landlock |
| darwin | **Seatbelt / `sandbox-exec`**，唯一候选、不 probe |
| win32 | **ACL restricted-token**（`dsh-sandbox-windows-acl`），enforcement 报 `partial` |

Landlock 启动器在 `native/landlock-run`：本体是 **C11** `main.c`（self-restrict-then-exec，规则集经 `execve` 继承；失败 **exit 125、不 exec** 目标）。npm 包名带 `node-addon`，但**不是** N-API addon，而是预编译静态二进制 + TS 包装（`grantArgs` → `--ro`/`--rw`）。macOS/Windows **没有** Landlock 移植。

`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 两个独立的服务键

普查里 `sandbox` 与 `sandboxPolicy` 是**两个**服务键。**"能力"和"策略"分开**——所以策略可以按部署替换而不换沙箱实现。

在工具侧能看到策略的痕迹（`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`——**有一套受控的提权路径**，而不是"要么全禁要么全开"。

### 18.4 一篇值得所有沙箱作者读的事后复盘

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

标题就是全部教训：**Landlock 的"部分生效"通知曾把子进程自己的失败错误分类**。

当沙箱只能部分生效（老内核不支持某些 rule），你发出的"部分生效"提示如果把**子进程自己的失败**也算进去，用户会以为是沙箱问题而去关掉沙箱——**一个诊断信息的分类错误，导致用户主动降低安全等级**。

dsh 一共只有 4 篇 postmortem，其中一篇给了沙箱的错误分类。这个选择本身说明了他们认为什么问题值得写下来。

---

<h2 id="ch19">第 19 章 typert：类型图驱动的 RPC，与 11 个包的野心</h2>

### 19.1 它是什么

`AGENTS.md`：

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

`packages/typert` 有 **11 个包 · 15 404 行**——比 goal、plan、todo、schedule、jobs、guard 加起来还大。这不是一个辅助工具，这是一套基础设施。

### 19.2 它解决什么问题

Web 前端（`client` 39 包 · 138 163 行）要调用后端的服务。传统做法：手写 REST/RPC 接口 + 手写类型定义 + 手动保持同步。

typert 的做法：**从 TypeScript 类型生成类型图，加载器和运行时注册表据此提供端到端类型安全的 RPC**。

配套文档 `docs/api-gateway.md` 与 `docs/subsystems/typert.md`。普查里能看到 `dsh-api-remotes` 被 23 个包依赖——前端各模块通过它访问后端。

### 19.3 为什么这算"野心"

因为它意味着 dsh 不打算只做一个 CLI。`api/` 的定位是 "**Remote BFF** assembly"（Backend For Frontend）——这是面向多客户端、多部署形态的架构。

`AGENTS.md` 的编译器面规则里还有一句相关的：

> **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` 是全仓唯一被允许有多个编译聚合的包——因为它要同时面向 host 面和 client 面。**例外被显式记录，而不是默默存在。**

---

<h2 id="ch20">第 20 章 四种前端一套行为：CLI / Web / ACP / SDK</h2>

### 20.1 四种壳

| 前端 | 包 | 定位 |
|---|---|---|
| **CLI** | `apps/cli`（拥有 `dsh` bin） | 交互终端 + `--profile headless` 一次性任务 |
| **Web** | `apps/web` + `client` 39 包（138 163 行） | 浏览器应用，默认 `127.0.0.1:3080` |
| **ACP** | `packages/acp`（1 669 行） | **automation-only** Agent Client Protocol 服务器 |
| **SDK** | `packages/sdk`（3 包）+ `python/sdk` | JSON-RPC 协议 + server + TS client + Python SDK |

`AGENTS.md` 特意标注 ACP 是 **"automation-only"**——它不是给人用的交互界面，而是给程序用的自动化接口。

### 20.2 Web Client 的扩展点

`docs/architecture.md` 扩展点表里有一行：

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

**前端也是插件化的**。会话里的每种节点（消息、工具卡、diff、终端输出）都是一个 `ConversationNodeDefinition` + 对应的 keyed renderer。

client 组 39 个包的分层（普查里的依赖入度）：`dsh-client-ui-slots` 35 次、`dsh-client-runtime` 34 次、`dsh-client-ui-primitives` 29 次、`dsh-client-locale` 27 次、`dsh-client-connection` 19 次。**UI slots 是前端的最大汇聚点**，对应后端的 `ctx.tools`。

### 20.3 一套行为怎么保证

靠**快照测试矩阵**（`docs/testing.md`）：

- ACP 场景：`examples/<name>/tests/snapshots/`，boot 真实自动化服务器示例、重放录制会话、diff 规范化的 JSON-RPC **加上**重新持久化的日志
- headless 后端场景：`examples/headless-agent` 拥有内部标准事件 JSONL 快照
- CLI 交互旅程：`apps/cli/tests/snapshots/`，JSONL 驱动
- Web 浏览器旅程：`apps/web/tests/snapshots/`，Chromium 重放（**Linux PR 必过门禁**）

而且有一条很讲究的规定：

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

**只有一个场景钉住完整的系统提示与工具 schema 内容，其它场景把它 token 化**——所以改一句提示词只会让一个快照变一行，而不是几十个快照全红。

这是快照测试规模化的关键技巧，值得单独抄走。

---

<div class="part-band"><span class="band-k">Part VI</span>241 个包全名册与协作图谱</div>

<h2 id="ch21">第 21 章 名册读法与六大功能域</h2>

### 21.1 这份名册怎么来的

我写了一个普查脚本（`scripts/dsh-plugin-census.py`），对 241 个包逐个提取：

| 字段 | 提取方式 |
|---|---|
| 包名 | `package.json` 的 `name` |
| 角色 | 由包名前缀 + 是否声明 `ctx` 键 + 是否注册工具推断 |
| 拥有的 `ctx` 键 | 源码里 `declare module '@deepseek-ai/cordis' { interface Context { … } }` |
| 注入的服务 | `export const inject = [...]` 与 `static inject = [...]` |
| 模型可见工具 | `defineTool({ name: '…' })` |
| 行数 | 该包下所有 `.ts`/`.tsx`（排除 `.d.ts`） |
| **它做什么** | **源码顶部 `@module` JSDoc 的第一段原文（翻译）** |

最后一列是这份名册最有价值的部分：**它不是我总结的，是包作者自己写的一句话职责**。241 个包里 227 个有这样的 JSDoc（缺失的 14 个是 fixture、平台二进制包和 vendored Cordis 插件）。

这也侧面证明了 `verify-export-jsdoc` 这条门禁的效果——**这个仓库的模块级文档覆盖率高到可以直接当名册用**。

### 21.2 角色分布

| 角色 | 数量 | 说明 |
|---|---|---|
| Provider/插件 | 97 | 实现某个 seam，或纯策略/胶水插件 |
| **Service Definition** | **60** | 拥有至少一个 `ctx` 键 |
| Web 客户端模块 | 39 | `client` 组 |
| **Consumer（工具）** | **24** | 注册模型可见工具 |
| 支撑库 | 13 | `util` / `test-support` |
| Bundle | 3 | 分发层 |
| 其它 | 5 | fixture 等 |

**60 个 Service Definition 拥有 73 个服务键**（少数包拥有多个键，如 `agent-loop` 同时拥有 `agentLoop` 与 `configuredAgentIdentities`）。

### 21.3 六大功能域

52 个包组按功能重排成六域，后面三章逐域展开：

```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">第 22 章 域一 · 主轴（30 包）与域二 · 上下文经济（11 包）</h2>

### 22.1 域一导读：五个键撑起整个产品

主轴 30 个包提供的核心服务键只有七个：`sessions` `systemPrompt` `tools` `agents` `agentLoop` `llm` + `scope`（库，无键）。但它们是所有其它插件的挂载面：

```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` 注入五个服务**（`agents` `llm` `sessions` `systemPrompt` `tools`，普查数据）——这五个就是"跑一轮对话"的全部依赖。而它自己被配置挂载，可替换。

域一的三个观察：

1. **`core/tools` 13 743 行是单包第二大**——工具注册表 + 五段流水线 + 呈现模式，复杂度集中在这里
2. **`session` 组 13 包**把"日志"拆成核心 + 持久化 + 投影 + 标题 + 遥测 + 引用解析，因为它们的演进节奏不同
3. **`session-query` 4 包 12 283 行**提供 5 个只读工具让模型检索自己的历史——**agent 能查自己的会话日志**，这是别家少见的能力

#### core · 产品 API 主轴（8 包 · 40,854 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `tools` | **Def** | Tool registry, model presentation modes, and pre/guard/around/post/result execution pipeline. | 拥有 `tools` · 注入 `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. | 拥有 `agentLoop`, `configuredAgentIdentities` · 注入 `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`). | 拥有 `sessions` | 8,401 |
| `agent` | **Def** | Agent service: live registry, factory delegation, and process-local initiator scope. Concrete creation and driving belong to the loop. | 拥有 `agent`, `agents` | 3,206 |
| `system-prompt` | **Def** | Registry for ordered system sections, dynamic context, tool schemas, and prompt variables. | 拥有 `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. | 拥有 `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. … | 注入 `tools` | 236 |

#### llm · 模型接入缝（5 包 · 20,502 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `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. | 拥有 `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  … | 注入 `llm` | 3,705 |
| `token-meter` | **Def** | Single replay-aware token-meter service for request and surface pressure. | 拥有 `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. | 注入 `agents` | 2,333 |

#### session · 持久会话日志（13 包 · 21,520 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 拥有 `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. | 注入 `sessions` | 4,570 |
| `session-title` | **Def** | Log-backed session title service, deterministic fallback, and provider contract. | 拥有 `sessionTitle` · 注入 `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`. | 注入 `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 … | 拥有 `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()`. … | 注入 `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 … | 拥有 `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`). … | 拥有 `sessionProjectionCache` · 注入 `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 … | 注入 `sessionProjections` | 688 |
| `session-checkpoint-policy` | Prov | Semantic durability checkpoints for model requests, top-level tool dispatch, and completed agent steps. | 注入 `llm`, `sessionPersistence`, `sessions`, `tools` | 559 |
| `session-title-first-prompt-llm` | Prov | First-human-message model provider for `ctx.sessionTitle`. | 注入 `llm`, `sessionTitle`, `sessions` | 339 |
| `session-title-all-prompts-llm` | Prov | All-human-messages model provider for `ctx.sessionTitle`. | 注入 `llm`, `sessionTitle`, `sessions` | 142 |

#### session-query · 日志检索（4 包 · 12,323 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `session-query-sqlite` | **Def** | Concrete session-query service with SQLite FTS5 over the live-preferred corpus. | 拥有 `launcherSessionQueryPath` | 3,936 |
| `session-query` | **Def** | Service Definition for combined session-history reads, traces, filters, and full-text search. | 拥有 `sessionQuery` · 注入 `sessions` | 3,770 |
| `tool-session-query` | **Tool** | Model-facing, workspace-authorized session-history search and read tools. | 注入 `sessionQuery`, `systemPrompt`, `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. | 注入 `commands` | 859 |

### 22.2 域二导读：四个键管上下文

见第 13 章。这一域只有 11 包 19 828 行，但它决定了长会话能不能活下来。

`context` 组（4 包 · 10 251 行）是"请求上下文插件"——决定什么东西以什么顺序进入一次请求。它与 `systemPrompt` 的分工是：`systemPrompt` 管**系统提示段**，`context` 管**运行时上下文投影**（回看第 8 章 `runtimeContext.project()`）。

#### context · 请求上下文装配（4 包 · 10,280 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `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. | 拥有 `sessionReferenceResolver` · 注入 `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. … | 注入 `agents` | 693 |

#### compaction · 上下文压缩（4 包 · 8,063 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `compaction-basic` | Prov | Basic replay-aware compaction backend. | 注入 `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}. … | 拥有 `compaction` | 1,800 |
| `compaction-tool-result-pruner` | **Def** | Replay-safe, model-free tool-result pruning service. | 拥有 `toolResultPruner` · 注入 `tokenMeter` | 681 |
| `command-compact` | Prov | Human-facing `/compact` command over the backend-independent compaction seam. | 注入 `commands`, `compaction` | 624 |

#### spill · 超量输出溢出（3 包 · 1,485 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 注入 `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. … | 拥有 `spillStore` | 225 |

<h2 id="ch23">第 23 章 域三 · 执行世界（40 包）与域四 · 任务治理（39 包）</h2>

### 23.1 域三导读：两个原语 + 八种能力

见第 17 章。这一域最重要的读法是**看依赖方向**：`fs` 与 `subprocess` 是原语，`shell` / `terminal` / `lsp` / `code-runtime` / 搜索工具都建在它们之上，`sandbox` 横切（包装 argv），`e2b` 是远程实证。

`web` 组（6 包 · 5 985 行）是另一个独立 seam：`ctx.web` 后面是可换的搜索/抓取 provider，模型只看到 `web_search` / `web_fetch` 两个稳定工具。

#### fs · 文件系统缝与策略（7 包 · 13,088 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `fs`, `systemPrompt`, `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. … | 注入 `subprocess`, `systemPrompt`, `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. | 注入 `fs`, `tools` · 工具 `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. … | 拥有 `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 … | 注入 `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 执行缝（9 包 · 10,453 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `shell`, `shellEnv`, `systemPrompt`, `tools` · 工具 `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. … | 注入 `shell`, `shellEnv`, `systemPrompt`, `tools` · 工具 `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. | 注入 `terminals`, `tools` · 工具 `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 … | 注入 `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. … | 注入 `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`). … | 拥有 `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. | 拥有 `shell` | 476 |

#### subprocess · 进程树（2 包 · 4,272 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 拥有 `subprocess` | 532 |

#### terminal · 持久 PTY（3 包 · 5,741 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `terminal-bash` | Prov | Persistent shell PTY backend over the subprocess terminal primitive, shared sandbox policy, bounded output, and provider-owned session cleanup. | 注入 `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. | 注入 `systemPrompt`, `terminals`, `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. | 拥有 `terminals` | 1,290 |

#### code-runtime · 代码模式运行时（2 包 · 3,982 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 拥有 `codeRuntime` | 444 |

#### lsp · 语言服务（3 包 · 5,528 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `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 … | 注入 `lsp`, `systemPrompt`, `tools` · 工具 `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. … | 拥有 `lsp` | 531 |

#### sandbox · 沙箱与策略（4 包 · 9,089 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 拥有 `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`). … | 拥有 `sandboxPolicy` | 581 |

#### e2b · 远程沙箱 POC（3 包 · 6,646 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `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. | 注入 `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. | 拥有 `e2b` | 647 |

#### web · 搜索与抓取（6 包 · 6,010 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `systemPrompt`, `tools`, `web` · 工具 `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. | 注入 `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. | 注入 `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 … | 注入 `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. … | 拥有 `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`. | 注入 `web` | 568 |

#### mcp · MCP 桥（1 包 · 3,333 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `tools` | 3,333 |

### 23.2 域四导读：治理插件的共同模式

39 个治理包有一个共同模式，我总结为**"确定性沉进工具，判断留给模型"**：

| 包 | 沉进工具的确定性部分 | 留给模型的判断 |
|---|---|---|
| `goal` | 轮次计数、CAS 校验、blocked 下界、人类根权限 | 目标是否真的达成 |
| `plan` | 模式状态机、step 边界记录、模式外调用拒绝 | 计划内容 |
| `todo` | 会话状态、并行策略 | 任务分解 |
| `schedule` | 时间语义、有界频率、持久化屏障 | 什么时候该定时 |
| `jobs` | 作业 id、归属、轮询、终止 | 什么该放后台 |
| `guard` | 循环卫生阈值、工具超时 | —— |
| `interaction` | 权限档位、审批一次性语义 | —— |
| `subagent` | 生命周期、后台投递、settlement | 委派什么给谁 |
| `workflow` | 工作流引擎、worker 线程 | 脚本内容 |

这套分工与本系列 MiMo Code（Goal 独立裁判）、grok-build（Goal Mode 五件套）、Reasonix（Delivery Profile 证据签收）、DeepTutor（掌握度门禁沉进工具）是同一个思想的不同实现。**dsh 的特色是每一条都是独立插件，循环对它们一无所知。**

#### skill · 技能（4 包 · 6,255 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `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  … | 拥有 `skills` | 2,172 |
| `tool-skill` | **Tool** | Durable session skill catalog and model-facing `skill` loader tool. | 注入 `agents`, `skills`, `tools` · 工具 `skill` | 1,549 |
| `skill-badge` | Prov | Bundled `dsh-badge` skill provider. | 注入 `skills` | 133 |

#### subagent · 子智能体缝（11 包 · 24,555 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 拥有 `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. | 注入 `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. | 注入 `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. … | 注入 `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}). … | 注入 `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()`. … | 注入 `subagents`, `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 … | 注入 `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. | 注入 `subagents`, `systemPrompt`, `tools` · 工具 `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). … | 注入 `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. … | 注入 `subagents` | 487 |

#### workflow · 工作流引擎（4 包 · 7,734 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `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 … | 注入 `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. | 注入 `subagents`, `systemPrompt`, `tools`, `workflowEngine` · 工具 `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. | 拥有 `workflowEngine` | 762 |

#### goal · 同会话目标（4 包 · 5,812 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `goal` | **Def** | Same-session goal domain: event-sourced state, compare-and-set mutations, and process-local continuation activation. | 拥有 `goals` · 注入 `agents` | 2,480 |
| `goal-round-driver` | Prov | Same-session goal-round driver over public agent, session, and goal services. | 注入 `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. | 注入 `agents`, `goals`, `systemPrompt`, `tools` · 工具 `create_goal`, `get_goal`, `update_goal` | 1,134 |
| `command-goal` | Prov | Human-facing `/goal` command over the persisted same-session goal domain. | 注入 `commands`, `goals` | 437 |

#### plan · 计划模式（1 包 · 2,036 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 拥有 `planMode` · 注入 `systemPrompt`, `tools` | 2,036 |

#### todo · 待办（1 包 · 1,012 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `tools` · 工具 `todo_write` | 1,012 |

#### schedule · 定时（1 包 · 4,314 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `schedule` | **Tool** | Agent-scoped durable one-shot and fixed-rate reminders over the session event log. | 注入 `agents`, `sessionPersistence`, `sessions`, `tools` · 工具 `schedule_create`, `schedule_delete`, `schedule_list` | 4,314 |

#### jobs · 后台作业（3 包 · 3,712 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 注入 `jobs`, `systemPrompt`, `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`. | 拥有 `jobs` | 613 |

#### guard · 循环卫生（2 包 · 1,027 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `tools` | 358 |

#### interaction · 审批/权限/命令/问人（5 包 · 4,225 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `commands` | **Def** | Plugin-owned human-command registry shared by interactive UI adapters. | 拥有 `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. | 拥有 `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. … | 拥有 `permissionPresets` · 注入 `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. | 拥有 `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. | 注入 `tools`, `userQuestions` · 工具 `ask_user_question` | 454 |

#### hooks · 外部 hook 协议桥（3 包 · 4,968 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `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 … | 注入 `shell` | 1,429 |

<h2 id="ch24">第 24 章 域五 · 装配与宿主（51 包）与域六 · 前端与支撑（70 包）</h2>

### 24.1 域五导读：从 YAML 到运行中的树

装配链路（第 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"]
```

三个观察：

1. **`dsh-base` 是一个 117 行、没有运行时 API 的包**——它的全部实质是一个 `cordis.patch.yml`。JSDoc 原话："this module carries no runtime API"。**配置即架构的字面体现。**
2. **`typert` 11 包 15 404 行**是这一域最大的投入，见第 19 章。
3. **`host` 组 8 包 22 412 行**提供宿主能力（`dshHomePath` `launchEnvironment` `cmdlineArgs` `directoryPicker` `appExit` 等键）——把"进程/机器"这一层也做成了服务。

#### preset · 逐会话能力装配（2 包 · 3,814 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 拥有 `agentPresets` · 注入 `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**. … | 注入 `systemPrompt` | 244 |

#### bundle · 分发层（3 包 · 1,424 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 注入 `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. | 注入 `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 · 启动胶水（2 包 · 4,094 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 拥有 `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. … | 拥有 `appExit`, `cmdlineArgs` | 437 |

#### host · 宿主能力（8 包 · 22,513 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 拥有 `apiProxy` · 注入 `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. … | 注入 `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. … | 拥有 `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. … | 注入 `webServer` | 280 |
| `host-plugin-inventory` | Prov | Read-only projection of the current Cordis Loader plugin entries. | 注入 `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 … | 拥有 `directoryPicker` | 206 |

#### api · 远程 BFF 与 RPC 网关（2 包 · 4,431 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `api-gateway` | Prov | Live Typert Remote dispatch over Cordis Services and registered providers. Transport, request correlation, and response envelopes belong to Connection. | 注入 `typert` | 3,586 |
| `api-remotes` | Prov | Host BFF entry and Loader shell for the Remote contribution assembly. | — | 845 |

#### typert · 类型图运行时（11 包 · 15,404 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 注入 `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` | 其它 | — | — | 364 |
| `@fixture/host` | **Def** | — | 拥有 `aliased`, `defaultOnly`, `demo`, `ignoredExternal`, `ignoredInline`, `ignoredPrimitive` | 305 |
| `@fixture/remote-workspace` | 其它 | — | — | 78 |
| `@fixture/remote` | Prov | — | — | 55 |
| `@fixture/client` | **Def** | — | 拥有 `clientBridge` | 40 |
| `@fixture/domain` | Prov | Host-only live Agent object. | — | 23 |
| `@fixture/write` | **Def** | Service whose public annotations are intentionally absent. | 拥有 `writable` | 19 |

#### sdk · JSON-RPC 协议与客户端（3 包 · 4,492 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `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 包 · 1,669 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 注入 `agents` | 1,669 |

#### settings · 用户设置（2 包 · 3,913 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 … | 拥有 `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 · 凭据引用（2 包 · 1,831 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 拥有 `credentials` | 377 |

#### identity · 匿名身份（1 包 · 252 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 · 存储域（4 包 · 3,428 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 拥有 `storageDomain` · 注入 `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. | 注入 `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. | 注入 `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. | 拥有 `storage` | 529 |

#### workspace · 工作区注册（1 包 · 2,155 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `workspace` | **Def** | Workspace entity registry (`ctx.workspaceRegistry`): durable workspace records, stable registry order, and header-validated session membership over the domain data form. | 拥有 `workspaceRegistry` · 注入 `sessionPersistence`, `storageDomain` | 2,155 |

#### attachment · 附件（2 包 · 921 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 | 拥有 `attachments` | 176 |

#### feedback · 消息反馈（2 包 · 2,167 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `message-feedback` | **Def** | Durable, lifecycle-bound feedback for finalized assistant messages. | 拥有 `messageFeedback` · 注入 `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. | 注入 `commands` | 504 |

#### runtime-diagnostics · 运行时不变量（1 包 · 543 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 拥有 `invariants` | 543 |

#### extensions · 扩展装配（4 包 · 20,305 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `cordisInspect`, `dynamicCordisRunner`, `systemPrompt`, `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. | 拥有 `dynamicCordisRunner` · 注入 `tools` · 工具 `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 域六导读：前端 39 包与"测试基础设施是一等公民"

**`client` 39 包 · 138 163 行**是全仓最大的组，占 TS/TSX 总量的 1/4。它自己也是分层的：ui-slots（35 次入度）→ ui-primitives（29）→ runtime（34）→ connection（19）→ locale（27）。

**`test-support` 6 包 · 13 344 行**值得单独说：测试基础设施被做成了**发布级的 workspace 包**（如 `dsh-acp-snapshot` 是一个"suite factory"）。多数项目的测试工具是散落在 `tests/helpers/` 里的脚本；dsh 把它们当产品对待。

`vendor` 9 包是 pinned 的 Cordis 源码（含 `cordis-plugin-loader` / `-hmr` / `-include` / `-group`），`native` 4 包是 Landlock 启动器按平台分包。

#### client · Web 客户端模块（39 包 · 138,163 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 注入 `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 … | 拥有 `clientModules` · 注入 `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. | 注入 `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 … | 注入 `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 · 产品装配（CLI / Web）（2 包 · 23,928 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 · 可运行示例装配（3 包 · 2,876 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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 · 测试基础设施（6 包 · 13,391 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. … | 注入 `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 · 零依赖工具库（7 包 · 2,225 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `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. | 拥有 `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 包 · 6,584 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `cordis` | Prov | Core context type and root context implementation. | — | 2,702 |
| `cordis-plugin-loader` | **Def** | — | 拥有 `entry`, `loader`, `startTime` | 1,161 |
| `schemastery` | Prov | — | — | 921 |
| `cordis-plugin-hmr` | **Def** | — | 拥有 `hmr` · 注入 `loader`, `timer` | 614 |
| `cosmokit` | Prov | Array set and normalization helpers. | — | 483 |
| `cordis-plugin-include` | Prov | — | 注入 `loader` | 378 |
| `cordis-plugin-logger-console` | Prov | Re-export shared console exporter config and base implementation. | — | 173 |
| `cordis-plugin-timer` | **Def** | — | 拥有 `timer` | 148 |
| `cordis-plugin-group` | Prov | — | — | 4 |

#### native · Landlock 原生启动器（4 包 · 343 行）

| 包 | 角色 | 它做什么（源码 `@module` JSDoc） | 接线 | 行数 |
|---|---|---|---|---|
| `node-addon-landlock-run-workspace` | 其它 | — | — | 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` | 其它 | — | — | 0 |
| `node-addon-landlock-run-linux-x64` | 其它 | — | — | 0 |

<h2 id="ch25">第 25 章 协作图谱：入度、装配链路、一次 turn 的插件参与表</h2>

### 25.1 依赖入度分层

普查数据画出的依赖层级（箭头 = 被依赖）：

```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
```

三层结构很清楚：

- **制度层**（`invariants` / `schemastery` / `brand` / `timeout`）：横切所有包，与业务无关
- **主轴层**（`session` → `agent` → `tools` → `system-prompt`，`llm` 横切）：业务核心
- **前端层**（`api-remotes` → `client-*`）：独立的依赖社区

### 25.2 一次 turn 的插件参与表

这是"它们如何互相配合"最直接的答案——同一次 turn 里，各插件在哪个时刻接入：

| 时刻 | 事件/钩子 | 谁在这里工作 |
|---|---|---|
| turn 打开 | `turn/start`（session 事件） | session 持久化、遥测、UI |
| 领取输入 | `inbox.claim` | 用户消息、工具附加上下文、jobs 完成通知、skill 目录注入 |
| 装配提示 | `systemPrompt.assemble` | 20 个包贡献段落：身份、工具 schema、cwd、skills 清单、todo、plan 约束、subagent 专属段 |
| 决定模型看什么 | `agent/pre-step` ⟨waterfall⟩ | plan-mode 拦截、context 投影、权限预检 |
| 定请求参数 | `agent/request` ⟨waterfall⟩ | 模型路由、reasoning effort、maxTokens、provider 特定参数 |
| 流式 | `llm/stream` ⟨waterfall⟩ | llm provider、token 计量、CoT 处理 |
| 每个分片 | `assistant/chunk`（session 事件） | UI 实时渲染、持久化 |
| 请求失败 | `agent/request-error` ⟨waterfall⟩ | 重试策略、provider 降级 |
| 工具调用前 | `tool/call`（session 事件）+ `presentCall` | UI 待处理卡片 |
| 工具准入 | `tools/pre-execute` ⟨waterfall⟩ | hooks 桥、permission、sandbox 策略、fs 观察策略 |
| 安全终局 | 单调守卫 | 不可放宽的拒绝 |
| 需要人 | `ctx.approval` | interaction 组的审批 UI |
| 工具执行 | `tools/execute` ⟨waterfall around⟩ | timeout、retry、metrics |
| 工具本体 | `ToolDefinition.execute` | 24 个 Consumer 包之一 |
| 变更意图 | `fs/write-intent` / `fs/edit-intent` | fs 策略插件 |
| 结果后处理 | `tools/post-execute` ⟨waterfall⟩ | spill（超量落盘）、pruner（修剪）、结果改写、附加上下文 |
| 结果定稿 | `tools/result` ⟨emit·冻结⟩ | 遥测、UI 完成卡片、invariant 检查 |
| step 关闭 | `step/end`（session 事件） | 统计 |
| 停机公告 | `agent/turn-stopping` ⟨serial⟩ | 同 turn 内异议（典型：`steer`）；**不是** goal 跨 turn 续跑 |
| idle 之后 | `agent/status` → `followup` | **goal-round-driver** 在 idle + armed 时开下一轮 |
| turn 关闭 | `turn/end`（session 事件） | 标题生成、持久化 flush、遥测汇总 |

**这张表就是插件化架构的价值证明**：19 个时刻，每个时刻都有独立插件接入，而 `agent.ts` 的 496 行里没有一行是为"审批""沙箱""溢出""目标""标题"写的。

### 25.3 三条典型装配链路

**链路一：`bash` 工具的完整依赖闭包**

```
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（间接）           写操作审批
```

一个工具，七个服务键，三层 provider。**而它的源码只有 2 081 行**，因为每一层都不需要它关心。

**链路二：一条"给模型加上下文"的合法路径**

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

**链路三：换到远程执行世界**

```
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>工程制度：本系列见过的最强一套</div>

<h2 id="ch26">第 26 章 逐文件 100% 覆盖率：把覆盖率当删码工具</h2>

### 26.1 六层测试

`docs/testing.md`（49 行，全是干货）：

| 层 | 命令 | 关键要求 |
|---|---|---|
| 单元 | `pnpm run test` | **每个注册表都要有 HMR 安全测试**（dispose 贡献 fiber，断言清理干净）；偏好边界、错误路径、事件顺序、并发竞态、契约回归的永久测试 |
| **覆盖率门禁** | `pnpm run test:coverage` | **`packages/*/*/src` 逐文件 100%** |
| 真实 API e2e | `pnpm run test:e2e` | 带 key 打真实 provider；各套件按自己的 key 自跳过 |
| **keyless 快照** | `pnpm run test:snapshot` | boot 真实示例、重放录制会话、diff 规范化 JSON-RPC + 重新持久化的日志 |
| 浏览器快照 | `pnpm run test:web` | Chromium 重放，**Linux PR 必过**；CI 强制只读 `DSH_SNAPSHOT=replay` |
| 门禁集合 | `doc-sync` / `hygiene` / `duplication` | 见第 27 章 |

### 26.2 技术闪光点：覆盖率的正确解读

`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.

**"未覆盖的行往往是死代码，门禁正确地在提示你删掉它"**——把覆盖率当**删代码的工具**，而不是写测试的 KPI。

这解释了第 8 章看到的 `/* v8 ignore next -- … */`：不可达的防御分支必须显式豁免 + 写清理由，而 `scripts/coverage-exempt.ts` + `coverage-exempt.spec.ts` 守着这些豁免本身。

也解释了第 2.2 节那条"不要过度防御"的规则——在 100% 覆盖率制度下，**每个多余的防御分支都有直接成本**。

### 26.3 一个诚实的例外

`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**.

没有 PowerShell 的机器上豁免该文件保持绿，CI 上装了 pwsh 强制满格。**例外被写进文档并给出理由**，而不是默默降低标准。

### 26.4 三条我认为最该抄的测试规矩

**① 验证世界，不验证自述**（`: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.

"对 agent 自己的输出做关键词探测会让作弊的 agent 通过"——这句话应该刻在每个 agent 项目的测试文件顶部。

**② 守卫必须被证明有效**（`:34`）：

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

**先引入回归、看它变红、再回滚**——这是我在本系列里见过的最高测试标准。

**③ 测真实入口路径**（`: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**).

用 tsx 跑源码会掩盖三类真实故障。关键路径必须用**发布产物**测。

### 26.5 with-key 政策：一句带公司身份的话

`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.**

"我们就是 DeepSeek，别省真实 API 测试"——这是全仓最有个性的一句话。而且它明确说：自跳过是为了让无密钥的贡献者不被阻塞，**不是成本信号**。

配套的还有"最高价值是 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">第 27 章 28 个 `verify-*`：把口头约定焊成机器门禁</h2>

### 27.1 宪法要求

`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.

两个要求：机械可检查的不变量**必须接到会被执行的顶层门禁**；而且要**证明每条被改的验收路径会拒绝非法输入**。

### 27.2 门禁清单

`scripts/` 下 124 个 `.ts`，其中 28 个 `verify-*`：

| 门禁 | 它把什么变成了机器可检查的 |
|---|---|
`verify-export-jsdoc` | 每个导出有 JSDoc；函数类导出有 `@param`/`@returns`
`verify-cordis-config` | `cordis.yml` 裸插件必须在 resolver manifest 的 `dependencies` 里
`verify-package-invariants` | 每包有 `./invariant` 伴生；空 installer 必须以 `No runtime invariant:` 解释
`verify-built-package-invariants` | 产物侧的同一条
`verify-type-equiv` | **文档里的 `ts type-equiv` 代码块必须与源码类型等价**
`verify-mermaid` | 图必须能渲染
`verify-md-links` / `verify-doc-refs` | 链接与引用不许烂
`verify-md-wrap` | markdown 换行纪律（一段一物理行）
`verify-doc-budgets` | **文档字数预算**（超了要改 manifest 的上限）
`verify-doc-site-fragments` | 文档站片段
`verify-package-readme-limitations` | **每个包 README 必须写清"局限"**
`verify-package-readme-model-experience` | **必须写清"模型体验"**
`verify-agent-note-classification` / `-format` | 设计文档的分类与格式
`verify-archived-agent-notes` | 归档笔记的冻结规则
`verify-translation-pairing` / `-prompt` | 双语文档逐段配对
`verify-config-source-ownership` | 配置源的所有权
`verify-client-domain-graph` | 前端域图
`verify-runtime-closure` | 运行时依赖闭包
`verify-node-next-types` | NodeNext 消费者类型
`verify-package-paths` | 包路径
`verify-dsh-package-licenses` | 许可证
`verify-public-repository-links` | 公开仓库链接
`verify-skill-invocation-metadata` | skill 调用元数据
`verify-vendored-links` | vendored 链接
`verify-md-*` 等 | 其余文档纪律

另有 `pnpm run hygiene`（knip 死代码 + publint 发布规范 + workspace 约束 + NodeNext 消费者检查）与 `pnpm run duplication`（jscpd 跨文件 TypeScript 克隆检测）。

### 27.3 三条本系列没见过第二家做的

**① `verify-package-readme-limitations`**：强制每个包的 README 写清**局限**。这防的是"过度宣传"——一个包的文档只说能做什么，不说不能做什么，使用者就会在错误的场景里用它。

**② `verify-package-readme-model-experience`**：强制写清**模型体验**。要求作者站在模型的视角描述自己的包：模型会看到什么、会怎么误用、schema 里的措辞会导致什么行为。**这是"为模型写文档"的制度化。**

**③ `verify-type-equiv`**：文档里的类型代码块必须与源码类型**等价**。所以 `docs/subsystems/*.md` 里那些 `ts type-equiv` 围栏块不是手抄的，是被机器校验过与源码一致的。

### 27.4 门禁自己也被守卫

`scripts/` 里能看到成对出现的文件：`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`……

**守卫也要被守卫。** 一个坏掉的门禁比没有门禁更危险，因为它给人虚假的安全感。

### 27.5 8 177 行生成的文档

`docs/` 里这些是**生成**的（由 `scripts/gen-*.ts` 产出、`doc-sync` 验证新鲜度）：

| 文档 | 行数 | 生成方式 |
|---|---|---|
`config-catalog.md` | 3 151 | 所有插件的 Config 字段
`tool-catalog.md` | 1 873 | **boot 每个工具插件读 `ctx.tools.schemas()`**
`module-graph.md` | 1 638 | 模块图
`persistence-catalog.md` | 944 | 持久化目录
`capability-seams.md` | 471 | 能力图
`event-producer-consumer.md` | 76 | 事件生产者/消费者
`graph-atlas.md` | 24 | 图索引

再加上每个子系统页面里的 "Cordis API" 区块（由 `gen-cordis-catalog.ts` 生成，**在两种语言侧字节相同**）。

**结论**：这个仓库的文档不是"写"出来的，是**从源码生成 + 机器校验新鲜度**。这也是为什么我能用一个普查脚本就抽出 241 个包的准确职责——**因为源码里的 JSDoc 是被门禁强制的**。

---

<h2 id="ch28">第 28 章 686 篇 Agent Notes：设计决策考古学的制度化</h2>

### 28.1 数字

`.agents/notes/` 下的英文主文件（每篇还有 `.zh.md` + `.i18n.yaml` 三件套）：

| 生命周期 | 篇数 | 含义 |
|---|---|---|
| `implemented/` | **507** | 决策已发货，且**与实际发货保持同步** |
| `archived/` | 143 | 已完成且理由不再指导未来工作，**冻结** |
| `proposed/` | 25 | 实现前被评审的提案 |
| `rejected/` | 11 | 被考虑后拒绝，**只在其理由能防止一个诱人的错误时保留** |
| **合计** | **686** | |

### 28.2 它是什么

`.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.

**"为什么"和"我们放弃了什么"**——这正是本系列分析方法论（动机 → 约束 → 被否方案 → 选择 → 代价）想从代码里反推的东西。**dsh 在源头就写下来了。**

### 28.3 双轴路径编码

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

- **lifecycle**（顶层目录）= 状态，笔记随状态**在目录间移动**
- **class**（嵌套目录）= 决策的**种类**，来自封闭集合（由 `scripts/agent-note-tree.ts` 定义，分类门禁拒绝其它文件夹）：

| class | 覆盖什么 |
|---|---|
`feature` | 新的用户或模型可见能力
`bug-fix` | 修缺陷或补 postmortem 暴露的漏洞
`simplification` | 删代码/行为/表面积而不加能力
`architecture` | 关于**发货源码**的结构决策
`process` | 代码**周边**的工具/策略/流程（门禁、包管理器、vendoring）
`testing` | 测试基础设施与策略

**`refactor` 故意缺席**，README 给了理由：

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

用一个**判别标准**（可观察行为是否改变）消灭一个模糊分类。这是分类学上的好品味。

### 28.4 四条很讲究的规则

**① implemented 必须与实际发货同步：**

> 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**).

只更新事实（路径、名字、结构），**不改决策本身**。所以你读一篇 2026-06 的笔记，里面的路径是当前的，但决策是当时的。

**② archived 是冻结的：**

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

归档时只允许：移动三件套、保留 `Status: implemented`、在状态下方插入同样的 `Archived: YYYY-MM-DD` 行（两种语言都插）、重录 sidecar、修复或删除入链。**这是唯一被允许的内容改动。**

**③ 交叉引用必须是相对 markdown 链接：**

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

不许写"见那篇关于 X 的笔记"，必须写链接。这样 `verify-doc-refs` 能检查，而且笔记在生命周期目录间移动后链接仍可修复。

**④ 不要集中索引：**

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

**连"不要加索引"这个决定本身都有一篇 Agent Note。** 这是制度自指的极致。

### 28.5 与 AGENTS.md 的配合

`AGENTS.md`：

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

而 `verify-agent-note-classification` / `verify-agent-note-format` / `verify-archived-agent-notes` 三条门禁守着这套制度。

### 28.6 11 个给 AI 协作者的 skill

`.agents/skills/`：

| skill | 作用 |
|---|---|
`dsh-prose-standard` | 文字标准（决定"contract/boundary/shape"这类词怎么用）
`dsh-pre-push-checks` | 推送前跑哪些检查
`dsh-code-review` | 代码评审
`dsh-doc-standards` / `dsh-doc-site-sync` | 文档标准与站点同步
`dsh-translate-docs` | 双语翻译（**只有用户显式调用才能跑**）
`dsh-archive-agent-notes` | 归档笔记的**校准过**工作流（README 明说：用这个而不是靠字数、年龄或配额）
`dsh-find-simplifications` | 找可简化处
`dsh-merging-stacked-prs` | 栈式 PR 合并
`dsh-trim-cot-leakage` | **修剪 CoT 泄漏**
`record-browser-gif` | 录制浏览器 GIF

### 28.7 一条罕见的散文纪律

`AGENTS.md` 的 "Type safety and documentation" 一节：

> 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`.

**"不要用隐喻"** + **"写 contract/boundary/shape 之前先问有没有更精确的词"**——它甚至给出了三组替换示例，并规定 `contract` 只留给"前置条件、后置条件、不变量、兼容承诺"这类**调用方真正依赖的义务**。

还有：

> **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.

**不要叙述控制流、不要保存评审历史、不要复述代码。** 保留行为、失败、时序、所有权、安全使用这五类事实，理由用链接。

这套散文纪律加上 `verify-doc-budgets`（字数预算），解释了为什么这个仓库 40 多份子系统文档读起来密度极高却不冗长。

---

<h2 id="ch29">第 29 章 动态 Cordis：agent 挂载自己的插件</h2>

### 29.1 它是什么

`AGENTS.md` 仓库布局仍写着 `self-modification/`。**仓库里没有这个目录。** `vitest.config.ts` 的 coverage exclude 也还列着 `packages/self-modification/*/src/**`。实际代码在 `packages/extensions/`：

| 包 | 职责 |
|---|---|
| `dsh-cordis-host-runner` | `ctx.dynamicCordisRunner`：define / run / stop / undefine |
| `dsh-tool-cordis` | 暴露给模型的 7 个工具 |
| `dsh-cordis-client-runner` / `dsh-ui-cordis` | Client 半与 UI |

对应的工具（普查抓到 7 个）：

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

`package.json` 里有一条 demo 脚本：`pnpm run demo:cordis` —— **"the agent modifies its own runtime"**。

### 29.2 安全边界

`docs/tool-catalog.md` 里 `dsh-tool-cordis` 那条部署说明很谨慎：

> **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.**

五个安全设计：

1. **不在任何发货树里**——刻意的 opt-in，因为动态包代码会触达真实运行时
2. 需要 `ctx.dynamicCordisRunner`（来自独立的 host-runner 包），它持有定义注册表**与 vm 沙箱**
3. 组合里缺这个服务，工具**永不激活**
4. 运行中的包可以注册**额外的模型可见工具**——直到它被 stop / undefine / DSH 重启
5. **工具集变化会被完整的 changed request header 记录到日志**——回到第 10 章的宪法：工具表是模型可见的，所以它的变化必须已记录

### 29.3 为什么这是"一切皆插件"的逻辑终点

如果一切都是插件，而 agent 能写代码，那 agent 就能写插件。而如果注册是可逆的 effect，那 agent 挂载的插件也能被干净地卸载。

**这个能力不是加上去的，是架构推导出来的。** 它同时也是对架构的压力测试：如果动态挂载一个插件会破坏什么，说明那个东西不该是特权状态。

本系列里最接近的是 DeepTutor 的 subagent（把编程 CLI 当 KB）和 Open Design（把 CLI 当引擎），但那两个是"调用外部 agent"。dsh 这个是**agent 修改自己的运行时装配**——量级不同。

---

<div class="part-band"><span class="band-k">Part VIII</span>品味与边界</div>

<h2 id="ch30">第 30 章 十六项决策五段式复盘</h2>

| # | 决策 | 动机 | 被否方案 | 选择 | 代价 |
|---|---|---|---|---|---|
| 1 | 一切皆插件、无特权内核 | 每个新需求都改主循环 | 主循环 + 钩子 | Cordis 服务仓库；循环也是插件 | 学习曲线陡；间接层多 |
| 2 | vendored Cordis | 框架与产品同节奏演进 | npm 依赖锁版本 | pinned 源码副本 + manifest + 同步流程 | 同步要重跑 test/build，本地修改要重新应用 |
| 3 | `inject` 表达加载顺序 | 手工启动顺序表不可维护 | 显式 boot 序列 | 不动点解析 + 未激活即报错 | 排查"为什么没加载"需要新工具 |
| 4 | 四种 dispatch 模式 | 一个 EventEmitter 塞不进四种语义 | 统一 emit | `@mode` 写进公共契约 + 生成目录校验 | 作者要判断该用哪种 |
| 5 | waterfall MUST 调 `next()` | 观察者短路会静默失效 | 靠约定 | 大写 MUST + 文档 + 测试 | 仍靠人守（无静态检查） |
| 6 | seam 三角色 | 换实现不该 fork 工具 | interface + if/else | Definition/Provider/Consumer，缺一不成缝 | 一个能力至少三包，仓库 241 包 |
| 7 | request/spec 分裂 | 隐含默认无法记录与断言 | `?? default` in `run()` | 显式 `resolve(request): Spec` | 多一个类型与一次转换 |
| 8 | append-only 日志为真相 | 内存 messages 丢一切 | 数组 + 顺便记日志 | event sourcing + `deriveMessages()` | 日志体积大；加上下文要扩事件表 |
| 9 | 模型可见 ⟺ 已记录 | 绕过日志会静默失真 | 靠 review | 218 包的 invariant 伴生 + 归属化失败 | 真实维护负担 |
| 10 | turn/step 两级边界 | 一级说不清"欠不欠" | 单层 while | step=一次请求+工具；turn=不欠为止 | 两个计数器与两个投递口 |
| 11 | `agent/turn-stopping` 公告 | 同 turn 内续跑不该改循环 | 循环里加条件 | serial 公告 + `steer` + 二次检查 inbox | 停机多一次事件往返；**跨 turn 的 goal 续跑另走 idle + `followup`** |
| 12 | 单调守卫 | waterfall 能被提权 | 只用 waterfall | 守卫只能 deny 或弃权 | 两层准入，作者要选对层 |
| 13 | 结果冻结 + emit | 观察者偷改已定稿结果 | 统一 waterfall | `Readonly` + `Object.freeze` + 不变量 | 后处理必须在 post-execute 完成 |
| 14 | 配置即架构（bundle/profile/patch） | 多形态前端不能三份 main | 环境变量 + if | 有序叠层 + 整体替换 + `--dump-config` | patch 是整体替换，写起来啰嗦 |
| 15 | 逐文件 100% 覆盖 | 死代码与假测试 | 总体百分比 | 逐文件满格 + 带理由豁免 | 需要豁免机制及其守卫 |
| 16 | 686 篇 Agent Notes | 代码与文档承载不了"为什么" | 靠 PR 描述 | 双轴路径 + 封闭分类 + 三条门禁 | 每个非平凡改动多一篇文档 |

---

<h2 id="ch31">第 31 章 横向对比：harness 型 vs 循环型</h2>

### 31.1 与本系列主要项目对比

| 维度 | DeepSeek Harness | Reasonix（同公司） | Claude Code | Codex | Open Design | DeepTutor |
|---|---|---|---|---|---|---|
| 架构形态 | **harness（无内核）** | 单二进制 + 传输无关控制器 | 循环型 | 循环型 | 宿主型（不写循环） | 双轨循环 + 三层插件 |
| 主循环地位 | **一个可替换插件** | 核心，单循环 | 核心 | 核心 | 不写 | 核心（两条） |
| 扩展方式 | 挂插件到 73 个服务键 | MCP + `use_capability` | 工具/权限/hooks | 工具/沙箱策略 | 适配器即数据 | 三层插件 |
| 上下文真相 | **append-only 日志 + 投影** | 稳定前缀 + turn tail | 会话 + 压缩 | 会话 | 剧本 | UnifiedContext |
| 能力替换 | **seam 三角色，换 provider 搬整个世界** | provider 适配 | 有限 | 有限 | 换 CLI | 换 RAG 引擎 |
| 权限模型 | pre-execute + **单调守卫** + approval | 应用层 | 权限规则 | OS 三平台围栏 | 无（外围补） | ToolMountFlags + 沙箱分级 |
| 沙箱 | `confine(argv)`：Linux bwrap→Landlock（C11 启动器）；macOS Seatbelt；Win ACL | 应用层 + OS | 权限 | Seatbelt/Landlock/Token | — | 三后端两级 |
| 工程门禁 | **逐文件 100% + 28 verify + 686 notes** | 常规 | 常规 | 常规 | 常规 | 常规 |
| 包数 | **241** | 单二进制 | — | — | — | 219（Python） |
| 许可 | MIT | MIT | 各异 | 各异 | 视仓库 | Apache-2.0 |

### 31.2 同一家公司的两条路

**Reasonix**：单静态二进制、缓存优先、传输无关的 `control.Controller`、soft→snip→summary 三阶压缩。目标是**省钱 + 可移植**。

**Harness**：241 包 monorepo、一切皆插件、73 个服务键、六层测试。目标是**可替换 + 可扩展 + 可长期维护**。

这不是矛盾，而是两个不同的产品假设：Reasonix 假设"用户要一个能塞进任何环境的终端 agent"，Harness 假设"生态要一个能被改造成任何形状的骨架"。

### 31.3 harness 型的适用边界

**harness 型赢在**：多前端一套行为、第三方扩展、能力整体替换（本机↔远程）、长期可维护性、把纪律焊进门禁。

**循环型赢在**：上手快、调试直观（栈里能看到全过程）、小改动成本低、不需要维护装配层。

一个具体判据：**你的产品要不要支持"用户替换掉执行环境"？** 如果要（远程沙箱、企业内网、自定义运行时），harness 型的 seam 投入会回本。如果不要，那 241 个包的间接层就是纯税。

### 31.4 三个别家没有的组合

1. **`dsh-invariants` 被 218/241 个包依赖**——运行时不变量是包的出生证明的一部分，而且规定了"不许写假装在检查的断言"。
2. **单调守卫**——用类型（没有 `allow` 返回值）保证安全策略在数学上不可能被后续插件放宽。
3. **686 篇双轴分类的 Agent Notes + 3 条门禁**——把"为什么这么设计、我们放弃了什么"制度化到有生命周期、有封闭分类、有归档冻结规则、连"不要加索引"都有一篇笔记。

---

<h2 id="ch32">第 32 章 诚实边界</h2>

1. **它是 developer preview**。README 用大写警告 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES"，`SESSION_FORMAT_VERSION` 保持 `0` 且**明确不做兼容承诺**，`AGENTS.md` 有专门的 "pre-release stance" 一节并给自己写了删除条件。**现在拿它做生产要有升级预算。**
2. **体量巨大**：497k 行 TS + 67k 行 TSX + 241 包。本分析抓宪法、主轴、协作图谱与制度，**不是逐包深读**——client 39 包 138k 行、typert 11 包 15k 行、extensions 4 包 20k 行都只做了名册级覆盖。
3. **学习曲线是真实成本**。读懂一次请求要同时装下服务仓库、inject 解析、四种 dispatch、effect 生命周期、seam 三角色、装配分层六件事。架构文档自己也说 "**We recommend using an agent to explore the codebase**"（`docs/architecture.md:7`）——**它建议你用 agent 来读它**。
4. **间接层让调试变难**。想知道 `ctx.shell` 是谁得看装配而非代码。所以才需要 `--dump-config`、`cordis_inspect_*`、`module-graph.md` 这一整套导航工具——**这些工具的存在本身就是代价的证据**。
5. **`verify-*` 门禁的维护成本**。28 个门禁 + 各自的 `.spec.ts` + 124 个脚本，这套体系需要人持续喂养。对小团队是净负担。
6. **名册的角色分类是我推断的**。普查脚本按"包名前缀 + 是否声明 ctx 键 + 是否注册工具"分类，与仓库自己的 `packages/README.md` 分组可能有差异；**职责描述那一列是源码 JSDoc 原文**，可信度更高。
7. **`inject` 提取不完整**。Cordis 声明依赖有多种写法（`export const inject`、`static inject`、Service 构造），我的脚本覆盖前两种，所以 101 个包被识别为"有 inject"可能低估。**包间依赖（229 个包有）那一列更可靠**。
8. **BENCHMARK.md 几乎是空的**。全文只说"跟着 Python SDK 指南跑 `jsonrpc-agent` 最小变体，用独立 workspace 与 session id"。**这个仓库的质量信心来自门禁与快照，不来自公开跑分。**
9. **没有验证过运行时行为**。本分析是静态阅读 + 交叉核对文档与源码，没有真跑 `pnpm dsh web`（需要 API key 与完整构建）。时序图与流水线图是从源码与生成文档推导的。
10. **行号锚定 `47f9438`**。这是一个日更频率极高的仓库（HEAD 就是当天的 merge commit，PR 编号已到 #2519），**行号会漂，回核请以符号名与文件名为准**。

---

<h2 id="ch33">第 33 章 源码导览索引与本地复现</h2>

### 33.1 🔍 源码指路表

| 你想理解… | 先读 |
|---|---|
| 架构宪法（给人和 AI 看的） | `AGENTS.md`（根；`CLAUDE.md` 是它的符号链接） |
| 架构全景 | `docs/architecture.md`（129 行，必读） |
| Cordis 五观念 | `docs/cordis-primer.md`（44 行，5 分钟） |
| 术语的严格定义 | `docs/glossary.md`（seam / scope / lineage…） |
| turn/step 循环 | `packages/core/agent-loop/src/agent.ts:212, 246, 332, 407` |
| Agent 接口与 inbox | `packages/core/agent/` |
| 会话日志与投影 | `packages/core/session/` + `packages/session/`（13 包） |
| 工具注册表与流水线 | `packages/core/tools/src/index.ts`；图见 `docs/tool-execution-pipeline.md` |
| 流水线顺序不变量 | `packages/core/tools/src/invariant.ts:94-115` |
| 系统提示装配 | `packages/core/system-prompt/` |
| per-agent scope | `packages/core/scope/` |
| seam 范本（三角色） | `packages/shell/`（9 包：1 Definition + 4 Provider + 3 Consumer + 1 env） |
| 一个执行世界 | `packages/fs/` + `packages/subprocess/`；实证 `packages/e2b/` |
| 沙箱 | `packages/sandbox/` + `native/landlock-run/` |
| 上下文经济 | `packages/compaction/` `packages/spill/` |
| 目标续跑 | `packages/goal/` |
| 子智能体 | `packages/subagent/`（11 包，含 codex / claude-code provider） |
| Code Mode | `packages/code-runtime/`；说明见 `docs/tool-catalog.md` 的 `run_code` |
| 类型图 RPC | `packages/typert/`（11 包）+ `packages/api/`；`docs/api-gateway.md` |
| 装配分层 | `packages/boot/` `packages/bundle/` `packages/preset/` |
| 不变量制度 | `docs/subsystems/invariants.md` + `packages/runtime-diagnostics/invariants/` |
| 测试宪法 | `docs/testing.md`（49 行，全是干货） |
| 门禁脚本 | `scripts/verify-*.ts`（28 个）+ `scripts/run-gates.ts` |
| 设计决策考古 | `.agents/notes/{proposed,implemented,archived,rejected}/`（686 篇） |
| 给 AI 的工作流 | `.agents/skills/`（11 个 SKILL.md） |
| 事故复盘 | `docs/postmortem/0001-0004` |
| 生成的目录 | `docs/{config,tool}-catalog.md` `module-graph.md` `capability-seams.md` `persistence-catalog.md` |
| 47 个工具的完整 schema | `docs/tool-catalog.md`（1 873 行） |

### 33.2 本地复现（只读分析）

```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
```

复现本文的 241 包名册（脚本在本仓库）：

```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（分组名册）
```

若要真的跑起来（非分析必需，需要 `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 和本系列其它文档的衔接

- 想动手造一个 → [从零构建插件化 Agent Harness](../从零构建插件化AgentHarness-DeepSeekHarness系-开发全流程教程.md)（20 步 · minidsh）
- 同公司另一条路 → [DeepSeek-Reasonix 源码分析](./DeepSeek-Reasonix-源码分析.md)（单二进制 + 缓存优先）
- 不写循环的极端 → [Open Design 源码分析](./open-design-源码分析.md)（把别人的 CLI 当引擎）
- 循环型的经典 → [Claude Code 工作原理科普（深入版）](../Claude-Code-工作原理科普-深入版.md)
- 沙箱与权限对照 → [openai-codex 源码分析](./openai-codex-源码分析.md)（三平台 OS 围栏）
- 三层插件与教育轴 → [DeepTutor 源码分析](./DeepTutor-源码分析.md)
- 工程手法横向 → [Agent 工程模式目录](../Agent工程模式目录.md)

---

## 附录 A · 版本基线指纹

| 项 | 值 |
|---|---|
| Repo | https://github.com/deepseek-ai/deepseek-harness |
| 版本 / Commit | `v0.1.0-rc.5`（`package.json`）/ `47f9438` |
| 许可 | MIT |
| workspace 包总数 | **241**（`packages/` 219 + vendor 9 + apps 2 + native 4 + 其它） |
| TypeScript | 497 489 行 / 2 319 文件 |
| TSX | 66 633 行 / 259 文件 |
| Python | 4 286 行 / 19 文件 |
| `ctx` 服务键 | **73** |
| 模型可见工具 | **47** |
| Service Definition 包 | 60 |
| Consumer（工具）包 | 24 |
| Provider/插件包 | 97 |
| 最大组 | `client` 39 包 · 138 163 行 |
| 最大单包 | `core/tools` 13 743 行 |
| 依赖入度冠军 | `dsh-invariants` ← **218 包（90%）** |
| `verify-*` 门禁 | **28**（`scripts/` 共 124 个 `.ts`） |
| Agent Notes | **686**（507 implemented / 143 archived / 25 proposed / 11 rejected） |
| 生成的文档 | 8 177 行 |
| postmortem | 4 篇 |
| `.agents/skills` | 11 |
| 底座 | vendored Cordis（9 包 · 6 584 行） |
| 覆盖率门禁 | `packages/*/*/src` **逐文件 100%** |

## 附录 B · 三个最独特设计（给总目录卡片用）

1. **没有特权内核**：模型适配器、工具注册表、会话日志、**乃至 agent 循环本身**都是插件，73 个 `ctx` 服务键可从配置替换；`--dump-config` 打印的每一行都能被自己的 patch 换掉。
2. **能改与只能看在类型层面分开**：三道 waterfall 可改写，`tools/result` 是 `emit` + `Readonly` + `Object.freeze` + 运行时不变量；安全边界另用**单调守卫**（返回类型里没有 `allow`，数学上无法被后续插件放宽）。
3. **纪律机械化到极致**：逐文件 100% 覆盖率（把未覆盖行当死代码信号）、28 个 `verify-*` 门禁（含强制每包 README 写"局限"与"模型体验"）、**`dsh-invariants` 被 218/241 包依赖**、686 篇双轴分类的 Agent Notes（连"不要加索引"都有一篇）。

## 附录 C · 分析范围声明

本分析基于 `47f9438` 的静态阅读，覆盖：架构宪法（`AGENTS.md` 全文）、`docs/` 全部纲领性文档、`core/` 主轴源码逐行、工具流水线与不变量源码、241 包全量普查（脚本可复现）、测试与门禁体系、Agent Notes 制度。

**不覆盖**：client 39 包与 typert 11 包的内部实现、每个 provider 的具体实现、686 篇 Agent Notes 的逐篇内容、`website/` VitePress 投影、Python SDK 内部、运行时实测（未跑 `pnpm dsh`）。`extensions` 组已核对 `tool-cordis` / `cordis-host-runner` 的职责与安全边界（第 29 章）；其余 extension 细节仍不逐行展开。

`AGENTS.md` 布局里的 `self-modification/` 与 `vitest.config.ts` 对该路径的 coverage exclude 是过时条目，以 `packages/extensions/` 为准。

行号相对该基线。这是一个 PR 编号已到 #2519、HEAD 为当日 merge 的高频演进仓库，**回核请以符号名与文件名为准**。
