# Open Design 源码分析：不写 Agent 的 Agent 产品——把「设计」变成一个可被任意 CLI 消费的文件系统

> **分析对象**：open-design（`nexu-io/open-design`），基于 **commit `c893b60`**（2026-07-28 01:48 UTC，`main`）。许可 **Apache-2.0**。
> **代码规模**：仓库 **11 745 个文件 / 321.9 MB**。其中 TypeScript `.ts` **2 341 个（56.7 MB）**、`.tsx` **455 个（9.7 MB）**、HTML **2 493 个（22.1 MB）**、Markdown **2 246 个（26.9 MB）**。核心三块：`apps/daemon/src/` 465 个 TS 文件（6.9 MB）、`apps/web/src/` 472 个（16.5 MB）、`packages/` 235 个（1.4 MB）。
> **社区体量**：**82.0k star / 9.5k fork / 701 open issue**，仓库 2026-04-28 创建——**三个月**。
> **一句话定位**：**Claude Design 的开源替身**。它自己**不实现任何 Agent 循环**，而是把你机器上已经装好的 `claude` / `codex` / `cursor-agent` / `copilot` / `opencode` 等 **26 条运行时定义（25 个不同的本地 CLI 可执行文件）** 当成「设计引擎」，喂给它们一套**技能 + 设计系统 + 工艺规则 + 插件**组成的文件系统，然后把产出的**真实文件**（HTML / PDF / PPTX / MP4）接回预览与导出。
> **读者对象**：已经读过本系列至少一份分析的读者。本文沿用「标注 `文件:行号`、可回源码核对」的写法，但**章节从 12 章扩到 18 章**——因为 Open Design 不是第十个编程 Agent，它是**这些 Agent 的宿主**，需要分析的层面完全不同。

---

> 🗺️ **配套架构图**：本项目在[**七大 Agent 架构图库**](../架构图库.html#ch3)里有一张专门的图——**多进程信任拓扑 + 扩展点图（接第 26 个 CLI 只改 1 个数据文件）**。
> 图库的每张图都先写清「回答什么问题」和「承重墙论点」，并经三轮审阅与渲染验收。

## 目录

**第一部分 · 它是什么**
1. [项目概览：一个不写 Agent 循环的 Agent 产品](#ch1)
2. [全景架构：四进程 · 四类内容 · 一条 SSE](#ch2)
3. [三种运行形态与「数据根」这条唯一真理](#ch3)

**第二部分 · 适配器层（最核心的一层）**
4. [适配器契约：`RuntimeAgentDef` 是数据规格，不是类](#ch4)
5. [探测与能力协商：为什么必须探测「将来真正会被 spawn 的那个路径」](#ch5)
6. [四种流格式：从 `claude-stream-json` 到 ACP 再到裸文本](#ch6)
7. [两档执行画像：`filesystem` vs `text_artifact`](#ch7)

**第三部分 · 上下文工程（真正的产品护城河）**
8. [输入分流：六个创建面 × 七种 skill mode](#ch8)
9. [心脏地带：`composeSystemPrompt` 的二十层与缓存分区](#ch9)
10. [三条硬规则：question-form → 品牌分支 → TodoWrite](#ch10)
11. [记忆与双环：task-brief / verify-scorecard / rule-proposal](#ch11)

**第四部分 · 内容平面与质量闸门**
12. [内容四平面：skills / design-templates / design-systems / craft](#ch12)
13. [质量闸门一：`lint-artifact` 程序化反 AI 味](#ch13)
14. [质量闸门二：Design Jury 五陪审员评审剧场](#ch14)
15. [插件与原子：`atoms` + `pipeline` + 封闭的 `until` 词汇表](#ch15)

**第五部分 · 边界与总结**
16. [安全边界：loopback / SSRF / 桌面 HMAC / `.od-skills` 拷贝](#ch16)
17. [功能特性拾遗：MCP 服务端、导出、HyperFrames、Live Artifact、自动化](#ch17)
18. [总结：三个最独特的设计与三处取舍](#ch18)

---

<h2 id="ch1">第 1 章 项目概览：一个不写 Agent 循环的 Agent 产品</h2>

### 1.1 它想解决的那个具体问题

2026 年 4 月，Anthropic 发布 **Claude Design**——第一次让大模型不再吐散文，而是直接交付**设计成品**。它火了，但它是闭源、付费、纯云、锁死在 Anthropic 的模型 / 技能 / 界面上：不能自托管，不能换 Agent，不能 Vercel 部署。

Open Design（下称 **OD**）就是它的开源替身。README 里那句定位很准：

> Open Design is what you get when the **agent-native** loop Anthropic shipped with Claude Design — discover the brief, lock the direction, stream the artifact, critique, deliver — stops being closed and becomes a **filesystem of functional skills, rendering design templates, design systems, and plugins** that the coding agents already on your laptop can read, write, and remix.

把这句话拆成三个可验证的工程主张：

| 主张 | 在代码里长什么样 |
|---|---|
| **Agent 无关** | `apps/daemon/src/runtimes/defs/` 下 **26 个对象字面量**，每个描述一条 CLI 怎么调；引擎零个 per-agent 分支 |
| **品牌可契约化** | `design-systems/` 下 **151 个包**，每包 `manifest.json` + `DESIGN.md` + `tokens.css`，被整段塞进系统提示词 |
| **交付真实文件** | Agent 在项目 cwd 里写文件，`FileViewer` 用沙箱 iframe 渲染，导出 HTML/PDF/PPTX/ZIP/MP4 |

### 1.2 与本系列前九个项目的根本差别

前面九份分析（opencode / hermes-agent / Raven / CodeWhale / goose / nanobot / grok-build / OpenManus / Suna / openworker）里，**每一个都在实现自己的主循环**：调模型、解析工具调用、执行、拼上下文、再调模型。它们争的是「谁的循环更好」。

OD **一行主循环都没有**。`docs/agent-adapters.md:5` 把这件事说得非常直白：

> We delegate the **entire agent loop** — model calls, tool use, context management, permission handling, resume, cancel — to the user's existing code agent CLI. OD's job is to detect it, feed it a skill + prompt + working directory, and stream its output back to the web UI.

以及那句立论（`docs/agent-adapters.md:9`）：

> **Thesis:** The code agent space has already converged on strong implementations (Claude Code, Codex, Devin for Terminal, Cursor Agent, OpenCode, Qoder CLI, and others). **Reimplementing another one is worse than talking to all of them.**

> 🧠 **一句话**：别的项目在造引擎；OD 在造**引擎的插座**，然后把全部精力砸在「引擎跑起来之后，怎么让它产出的东西像人做的」——也就是提示词工程 + 内容库 + 质量闸门这三件事。

### 1.3 数字化的项目形状

```
open-design/  (commit c893b60 · Apache-2.0 · 11 745 files · 321.9 MB)
├── apps/
│   ├── daemon/       465 TS 文件 / 6.9 MB   ← 产品权威：/api/*、SQLite、spawn、提示词组装
│   │   └── src/routes/ 32 个路由模块
│   ├── web/          472 TS/TSX / 16.5 MB   ← Next.js 16 + React 18，i18n 覆盖 20+ 语言
│   ├── desktop/       56 文件 / 1.1 MB      ← Electron 壳 + sidecar IPC
│   ├── packaged/      39 文件               ← 打包后的 daemon/web 侧车启动器
│   └── landing-page/ 221 文件 / 25 MB       ← Astro 官网（不参与运行时）
├── packages/         235 TS / 1.4 MB
│   ├── contracts/    95 个源文件            ← 唯一的 web↔daemon↔CLI 共享 DTO + 提示词镜像
│   ├── plugin-runtime/ · host/ · sidecar/ · registry-protocol/ · metatool/ …
├── skills/           164 个功能技能目录
├── design-templates/ 115 个渲染模板目录
├── design-systems/   151 个品牌包（`manifest.json` + `DESIGN.md` + `tokens.css`）
├── craft/            11 个通用工艺规则 .md（+README+FUTURE_SECTIONS）
├── plugins/
│   ├── _official/    scenarios 13 · atoms 13 · design-systems 143 · image-templates 45
│   │                 · video-templates 64 · examples 183
│   └── community/    9 个社区插件
├── prompt-templates/ image 49 · video 58
├── tools/            pack（打包）· dev（生命周期）· release · serve
├── e2e/              Playwright + Vitest，143 个文件
├── mocks/            mock-agent.mjs：不烧模型额度就能回放 CLI 流
├── clipper/          Chrome 扩展：抓网页品牌
└── figma-plugin/     Figma 侧插件
```

**几个值得先记住的量级**：
- **26 条运行时定义**（`registry.ts:30-57`），对应 **25 个不同的本地 CLI 可执行文件**——`byok-opencode` 复用 OpenCode 的可执行文件，所以定义数比二进制数多一个。
- **提示词的代码量比引擎还大**：`apps/daemon/src/prompts/system.ts` **2 075 行 / 125 KB**；镜像版 `packages/contracts/src/prompts/system.ts` **1 064 行 / 68 KB**。相比之下 `runtimes/registry.ts` 只有 **81 行**。
- **`apps/web/src/components/FileViewer.tsx` 单文件 660 KB**——这是整个仓库最大的源文件，预览/检查/评论/导出全在里面。

---

<h2 id="ch2">第 2 章 全景架构：四进程 · 四类内容 · 一条 SSE</h2>

### 2.1 进程拓扑

`docs/architecture.md:57-73` 给了一张纯文本图，翻译成 mermaid 并补上被省略的细节：

```mermaid
flowchart TB
    subgraph P1["① 渲染进程 · 浏览器 或 Electron renderer"]
        UI["Next.js 16 App Router + React 18<br/>聊天 · 文件工作区 · 沙箱 iframe 预览 · 设置 · 市场"]
    end
    subgraph P2["② Web 侧车 · Next.js server"]
        WEB["静态 UI + 预览状态<br/>/api/* · /artifacts/* · /frames/* 重写到兄弟进程"]
    end
    subgraph P3["③ Daemon · Node 24 + Express + better-sqlite3"]
        API["/api/* 路由（32 个模块）"]
        DB[("SQLite<br/>projects · conversations · messages<br/>runs · run_devloop_iterations · memory")]
        COMP["提示词组装器<br/>prompts/system.ts 2075 行"]
        REG["运行时注册表<br/>AGENT_DEFS 26 条"]
        LINT["lint-artifact 反 AI 味"]
        JURY["Critique Theater 五陪审"]
    end
    subgraph P4["④ 被 spawn 的 CLI 子进程（不是 OD 的代码）"]
        CLI["claude / codex / cursor-agent / copilot /<br/>opencode / devin / hermes / kimi / pi / …"]
    end
    subgraph P5["⑤ 桌面壳 · Electron main（可选）"]
        SHELL["原生窗口 · 自动更新 · 原生文件夹选择器<br/>sidecar IPC: STATUS/EVAL/SCREENSHOT/CONSOLE/CLICK/SHUTDOWN"]
    end

    UI -->|同源 HTTP + SSE| WEB --> API
    SHELL --> WEB
    SHELL -.->|HMAC 单次令牌| API
    API --> DB
    API --> COMP --> REG
    REG -->|spawn cli, cwd = 项目工作区| CLI
    CLI -->|stdout: 结构化事件 或 裸文本| API
    CLI -->|原生工具| FILES["项目文件（真实磁盘）"]
    API -->|SSE 规范化事件| UI
    API --> LINT
    API --> JURY
    FILES -->|文件事件| UI
```

四条必须记牢的边界：

1. **Web UI 和 `od` CLI 调的是同一套 daemon HTTP API。** `docs/architecture.md:75-77` 明确：「The CLI is not a second business-logic implementation; it is the machine-readable surface for the same capabilities.」这就是仓库反复强调的 **dual-track rule**（`apps/daemon/AGENTS.md:99-106`）：加一个用户能感知的能力，必须同一次改动里补齐 contract 类型 + daemon 路由 + web 界面 + `od` 子命令。
2. **协议是 HTTP + SSE，不是 WebSocket。** 架构文档开头专门有一段「Historical note」（`docs/architecture.md:11-16`）说明最早的草案画过 Vercel 隧道模式、浏览器直连模式、WebSocket `session.generate`、内存 session bus、`history.jsonl`、三根监听式技能注册表——**全部被实现推翻**。这段「否定式文档」在开源项目里很罕见，也是判断一份 spec 是否可信的好信号。
3. **Daemon 是唯一特权进程。** 它绑 loopback，拥有 SQLite、凭证、项目文件、spawn 权。web 只是壳。
4. **子进程不是 OD 的代码。** 这是最容易搞混的一点：当你在 OD 里看到「思考中 / 调用工具 / 写文件」，那是 Claude Code 或 Codex 在跑它们自己的循环，OD 只是把它们的 stdout 翻译成统一事件画出来。

### 2.2 内容四平面

OD 把「让模型做出好设计」拆成四条互相正交的轴（`craft/README.md:9-16` 的表格是最清楚的官方表述）：

| 轴 | 目录 | API | 管什么 | 例子 |
|---|---|---|---|---|
| **功能技能** | `skills/`（164） | `GET /api/skills` | Agent 干活时调用的**能力** | `brand-extract` · `web-clone` · `design-brief` |
| **渲染模板** | `design-templates/`（115） | `GET /api/design-templates` | 可渲染的**成品形状** | `saas-landing` · `dashboard` · `html-ppt-*` |
| **设计系统** | `design-systems/`（151） | `GET /api/design-systems` | **品牌契约**：色板、字体、间距、组件 | `apple` · `stripe` · `linear-app` |
| **工艺规则** | `craft/`（11 个 `.md`） | 由 daemon 按需组装 | **与品牌无关的普适规则** | ALL CAPS 必须 ≥0.06em 字距 |

第四轴 `craft/` 是 OD 相对 Claude Design 最聪明的一处拆分。`craft/README.md:18-21` 解释得很好：

> `DESIGN.md` tells the agent which colors and fonts a brand uses. `craft/` tells the agent the universal rules a competent designer applies on top — e.g. ALL CAPS always needs ≥0.06em tracking, regardless of the brand.

技能通过 frontmatter **按需订阅**工艺规则（`docs/skills-protocol.md:245-251`）：

```yaml
od:
  craft:
    requires: [typography, color, anti-ai-slop]
```

**只有被列出的段落会进提示词**——一个只排版的技能不用为色彩、动效内容付 token 成本。11 个已发布 slug：`typography` · `typography-hierarchy` · `typography-hierarchy-editorial` · `color` · `anti-ai-slop` · `state-coverage` · `animation-discipline` · `accessibility-baseline` · `rtl-and-bidi` · `form-validation` · `laws-of-ux`。

> 🧠 **一句话**：把「品牌怎么长」（DESIGN.md，会变）和「专业设计师的肌肉记忆」（craft，不会变）拆开，是 OD 让 151 个品牌包不互相抄袭同一份排版常识的关键。

### 2.3 注册表的「用户根优先」扫描

四类内容都遵循同一条发现规则（`docs/skills-protocol.md:125-138`、`docs/architecture.md:139-143`）：

```mermaid
flowchart LR
    REQ["GET /api/skills"] --> SCAN1["① 扫描用户可写根<br/>（daemon 数据根下）"]
    SCAN1 --> SCAN2["② 扫描内置根<br/>skills/"]
    SCAN2 --> MERGE["合并：同名 id 用户条目<br/>遮蔽内置条目（不删除）"]
    MERGE --> RESP["返回列表"]
```

三个刻意的设计：
- **每次请求都重扫**——没有 watcher、没有 `SIGHUP`、不用重启 daemon。你往目录里丢一个文件夹，刷新页面就出现。
- **遮蔽而非覆盖**：同名用户条目**遮蔽**内置条目，内置文件不动。删掉用户版，内置版自动回来。
- **聊天期解析跨两个注册表**：因为持久化的项目里 `skillId` 既可能是功能技能也可能是设计模板（`docs/architecture.md:141-143`）。这是历史包袱换来的兼容性。

---

<h2 id="ch3">第 3 章 三种运行形态与「数据根」这条唯一真理</h2>

### 3.1 三种形态

| 形态 | 入口 | 谁起 daemon | 谁起 web | 桌面壳 |
|---|---|---|---|---|
| **源码开发** | `pnpm tools-dev run web` | tools-dev 侧车 | tools-dev 侧车 | 不启 |
| **打包桌面 / 打包无头** | `apps/packaged` | packaged 启动器 | packaged 启动器 | Electron（无头版跳过） |
| **容器 / daemon 直服** | `docker compose up -d` | 单进程 | **同一个 daemon 直接服 `apps/web/out` 静态导出** | 无 |

`docs/architecture.md:22-31` 有一条很实在的工程约束：

> When ports are not supplied, `tools-dev` chooses available daemon and web ports. … **Ports are transport details; they do not define process identity, namespaces, or daemon data roots.**

也就是说：端口是临时的，**身份不是端口决定的**。打包桌面模式下，Electron 甚至**不假设端口**——它通过 sidecar IPC 去问 web 的真实 URL（`docs/architecture.md:36-39`）。这跟 openworker 用固定 8765 端口 + token 文件的做法形成对照：OD 把端口彻底降级成传输细节。

### 3.2 「数据根契约」：一处刻意的文档纪律

这是全仓库最有意思的一条**元规则**。`docs/architecture.md:160-162`：

> This document intentionally gives no concrete daemon data path. The root `AGENTS.md` section **Daemon data directory contract** is the only path authority.

README 里也重复了同一句禁令（「This README MUST NOT restate it」）。`apps/daemon/AGENTS.md:97` 再补一刀：「Route all daemon-owned data through `RUNTIME_DATA_DIR` or constants derived from it.」

为什么值得单独拎出来讲？因为**多处文档各自写死一个路径**是所有本地优先应用的经典腐烂源：改了实现，八个 md 里有六个还写着旧路径，用户按文档找不到数据。OD 的做法是把路径**降级成单点权威**，其他所有文档只允许引用不允许复述。

启动时 `apps/daemon/src/server.ts` 把 `OD_DATA_DIR` 解析一次成 `RUNTIME_DATA_DIR`，之后 SQLite、托管项目工作区、artifacts、用户注册表条目、凭证、自动化状态、插件状态**全部由这个根派生**（`docs/architecture.md:150-155`）。

**唯一的例外是文件夹导入**：导入的项目用用户选中的**外部** `metadata.baseDir`，daemon 对那个工作区做校验和边界限制，而不是把它拷进托管根（`docs/architecture.md:157-159`）。

```mermaid
flowchart TB
    ENV["OD_DATA_DIR 环境变量"] -->|启动时解析一次| ROOT["RUNTIME_DATA_DIR"]
    ROOT --> D1["SQLite 库"]
    ROOT --> D2["托管项目工作区"]
    ROOT --> D3["artifacts"]
    ROOT --> D4["用户 skills / templates / design-systems"]
    ROOT --> D5["凭证 · 自动化状态 · 插件状态"]
    IMP["POST /api/import/folder"] -->|唯一例外| EXT["用户选中的外部 baseDir<br/>（校验 + 边界限制，不拷贝）"]
```

---

<h2 id="ch4">第 4 章 适配器契约：<code>RuntimeAgentDef</code> 是数据规格，不是类</h2>

这一章是整个项目**最值得学的一层**。

### 4.1 反直觉的设计决策

面对「要支持 25 个 CLI」这个需求，99% 的团队会写一个抽象基类：

```ts
abstract class AgentAdapter {
  abstract run(prompt: string): AsyncIterable<Event>;
  abstract cancel(): void;
  abstract detect(): Promise<boolean>;
}
class ClaudeAdapter extends AgentAdapter { /* 200 行 */ }
class CodexAdapter  extends AgentAdapter { /* 200 行 */ }
// × 25
```

OD 没有。`docs/agent-adapters.md:17` 的原话：

> An adapter is **not** a class that implements the agent loop. It is a **plain data object** — one `RuntimeAgentDef` object literal per CLI — that declares *how to talk to* that CLI… **There is no per-agent subclass and no `run()` / `cancel()` method to implement.**

契约本体在 `apps/daemon/src/runtimes/types.ts:101-253`，共 **~45 个字段**，其中只有 **6 个必填**：

```ts
export type RuntimeAgentDef = {
  id: string;                          // 唯一键，注册表用它去重
  name: string;                        // 显示名
  bin: string;                          // 要在 PATH 上探测的可执行文件
  versionArgs: string[];               // 版本探测参数
  fallbackModels: RuntimeModelOption[];// 探测不到模型时的静态兜底
  buildArgs: (prompt, imagePaths, extraAllowedDirs?, options?, runtimeContext?) => string[];
  streamFormat: string;                // 引擎据此分派到解析器
  // …剩下 ~38 个可选字段，全部是数据或纯函数
};
```

**关键性质**：`buildArgs` 是唯一的函数字段，而且它是**纯的**——输入提示词和选项，输出 argv 数组。它不 spawn、不读文件、不管生命周期。所以：

- **探测**由 `detection.ts` 统一做；
- **启动**由 `launch.ts` 统一做；
- **调用**由 `invocation.ts` 统一做；
- **取消**由通用 run 生命周期统一做；
- **解析**由 `streamFormat` 分派到 4 个解析器之一。

`docs/agent-adapters.md:27` 因此可以给出这个承诺：

> **Adding a CLI is a one-file change.** Drop a new `runtimes/defs/<cli>.ts` exporting one `RuntimeAgentDef`, add it to the `BASE_AGENT_DEFS` array in `registry.ts`, and the engine detects, launches, invokes, and (for an existing `streamFormat`) streams it — **no engine edits, no new class, no method overrides.**

### 4.2 注册表：81 行 + 一条启动期不变式

`apps/daemon/src/runtimes/registry.ts` 全文只有 81 行，值得整段看结构：

```ts
// registry.ts:30-57 —— 26 条基础定义
const BASE_AGENT_DEFS: RuntimeAgentDef[] = [
  amrAgentDef, claudeAgentDef, codexAgentDef, devinAgentDef,
  opencodeAgentDef, byokOpenCodeAgentDef, hermesAgentDef, traeCliAgentDef,
  grokBuildAgentDef, kimiAgentDef, cursorAgentDef, qwenAgentDef,
  qoderAgentDef, copilotAgentDef, ampAgentDef, piAgentDef,
  kiroAgentDef, kiloAgentDef, vibeAgentDef, deepseekAgentDef,
  aiderAgentDef, antigravityAgentDef, reasonixAgentDef, codebuddyAgentDef,
  mimoAgentDef, atomcodeAgentDef,
];

// registry.ts:65-68 —— 用户本地 profile 合并进来
export const AGENT_DEFS: RuntimeAgentDef[] = [
  ...BASE_AGENT_DEFS,
  ...readLocalAgentProfileDefs(BASE_AGENT_DEFS),
];

// registry.ts:70-76 —— 启动期不变式：id 不许重复，重复直接 throw
const ids = new Set();
for (const def of AGENT_DEFS) {
  if (ids.has(def.id)) throw new Error(`Duplicate agent definition id: ${def.id}`);
  ids.add(def.id);
}
```

那个 `throw` 是**模块加载期**执行的——不是运行时检查，是**加载即失败**。一个用户自定义 profile 撞了内置 id，daemon 起不来，而不是安静地覆盖掉内置适配器。这是「让错误尽早、尽响地暴露」的教科书写法。

`readLocalAgentProfileDefs`（`local-profiles.ts`）允许用户在配置里定义**本地 profile**——比如同一个 `claude` 二进制配不同的 base URL、不同的默认模型，注册成两条不同的适配器。它们**继承基础适配器的 `authProbe`**，但通过 `authProbe.classifierAgentId`（`types.ts:244`）把「用谁的鉴权分类器」这件事显式带上，避免落回通用分类器。

### 4.3 一条真实的定义：`claude.ts` 逐行拆

`apps/daemon/src/runtimes/defs/claude.ts` 全文 98 行，是理解整个契约最好的样本。

```ts
// claude.ts:16-24
export const claudeAgentDef = {
    id: 'claude',
    name: 'Claude Code',
    bin: 'claude',
    fallbackBins: ['openclaude'],   // ← 见下方「兜底二进制」
```

**① 兜底二进制（`claude.ts:20-24`）**。注释写明了动机：

> Drop-in forks that ship a CLI argv-compatible with `claude`. Tried in order if `claude` itself isn't on PATH, so users on a single-binary install (e.g. only OpenClaude — issue #235) get auto-detected without writing wrapper scripts.

一行数组解决了「生态里出现 argv 兼容分叉」这个问题，不需要用户写 wrapper 脚本。

**② 能力探测（`claude.ts:30-39`）**：

```ts
    helpArgs: ['-p', '--help'],
    capabilityFlags: {
      '--include-partial-messages': 'partialMessages',
      '--add-dir': 'addDir',
    },
```

注释里藏着一个真实 bug 的尸检报告：

> `--add-dir` and `--include-partial-messages` live under `claude -p` subcommand, so we probe `claude -p --help` instead of `claude --help`. **Fixes issue #430: --add-dir never detected because it wasn't in global help.**

`capabilityFlags` 是「子串 → 能力键」的映射：探测 `--help` 输出后，命中的子串就把 `agentCapabilities[id][key]` 置 `true`。`buildArgs` 再读它决定加不加这个 flag。

**③ 模型发现（`claude.ts:40-44`）**：

```ts
    fallbackModels: CLAUDE_FALLBACK_MODELS,
    fetchModels: async (_resolvedBin, env) => loadMmdRouteModels(env, CLAUDE_FALLBACK_MODELS),
```

注释：`claude` 没有 list-models 子命令，所以优先读本地 mmd/MMS 路由（让代理背后的 Claude 兼容模型也出现在选择器里），再用内置别名兜底。

**④ 真正的 argv 构建（`claude.ts:52-89`）**——这是全定义的核心：

```ts
    buildArgs: (_prompt, _imagePaths, extraAllowedDirs = [], options = {}, runtimeContext = {}) => {
      const caps = agentCapabilities.get('claude') || {};
      const args = ['-p', '--input-format', 'stream-json',
                    '--output-format', 'stream-json', '--verbose'];
      if (caps.partialMessages) args.push('--include-partial-messages');   // 能力门
      if (options.model && options.model !== 'default')
        args.push('--model', options.model);
      const dirs = (extraAllowedDirs || []).filter(d => typeof d === 'string' && d.length > 0);
      if (dirs.length > 0 && caps.addDir !== false) args.push('--add-dir', ...dirs);
      if (runtimeContext.resumeSessionId)      args.push('--resume', runtimeContext.resumeSessionId);
      else if (runtimeContext.newSessionId)    args.push('--session-id', runtimeContext.newSessionId);
      args.push('--permission-mode', 'bypassPermissions');
      return args;
    },
```

注意 `_prompt` 前面那个下划线——**提示词根本不进 argv**。为什么？`claude.ts:45-51` 的注释是全仓库最实用的一段跨平台知识：

> Prompt delivered via stdin to avoid both Linux `spawn E2BIG` (**MAX_ARG_STRLEN caps a single argv entry at ~128 KB**) and Windows `spawn ENAMETOOLONG` (**CreateProcess caps the full command line at ~32 KB direct, ~8 KB via .cmd shim**).

**⑤ 会话续跑（`claude.ts:78-86`）**：

```ts
      // Continue Claude's own CLI session across turns so it keeps its
      // working memory (files read, edits made, tool history) instead of
      // re-deriving everything from the rendered transcript each turn.
```

这条设计非常重要，第 6 章会展开：OD **不是**每回合把全部对话历史重新拼给 CLI，而是**让 CLI 保留自己的会话**。

**⑥ 剩下三个声明字段（`claude.ts:90-97`）**：

```ts
    promptViaStdin: true,
    promptInputFormat: 'stream-json',
    streamFormat: 'claude-stream-json',
    externalMcpInjection: 'claude-mcp-json',   // Claude Code 会自动加载 cwd 的 .mcp.json
    resumesSessionViaCli: true,
} satisfies RuntimeAgentDef;
```

### 4.4 三种「会话续跑」风格

`types.ts:189-209` 用三个布尔字段区分了三种截然不同的续跑机制，这是我在其他项目里没见过的精细分类：

| 字段 | 风格 | 谁生成 session id | 代表 |
|---|---|---|---|
| `resumesSessionViaCli` | **指定式**（specify-style） | **daemon 生成** `newSessionId`，告诉 CLI 用它 | `claude --session-id <uuid>` |
| `capturesSessionIdFromStream` | **捕获式**（capture-style） | **CLI 自己生成**，从流里报出来 | `codex` 的 `thread.started.thread_id` |
| `resumesSessionViaAcpLoad` | **ACP 式** | 从 ACP 会话拿 `getDurableSessionId()` | AMR / Vela，用 `session/load` 续 |

注释（`types.ts:191-199`）说明了为什么必须分开：指定式的 id 是 daemon 存的那个；捕获式必须从解析后的流（表面为 `status` 事件的 `sessionId`）抓，`newSessionId` **根本不传给 CLI**。混淆这两者会导致「以为在续跑，实际每次都新开」。

还有一个更微妙的字段 `resumesSessionViaCli` 的**副作用**（`types.ts:70-84` 的长注释，讲的是 `agy -c`）：

> Without this opt-out, agy with `-c` receives the same prior turn **twice** — once from its own conversation memory, once embedded in the composed user request — and the embedded copy includes the literal `<question-form>` markup it emitted on turn 1. **The model then pattern-matches that and re-emits the form on turn 2, looking like the discovery loop never breaks.**

这是一个极具体的 bug 尸检：**双份上下文**导致模型在第二回合重新弹出发现表单，看起来像「循环卡住了」。解法不是改提示词，而是加一个 opt-out 标记，让 daemon 在这类适配器上**跳过渲染的 web transcript**，只发最新一条用户消息。

### 4.5 契约的边界：它刻意不包含什么

`docs/agent-adapters.md:176-181` 有一段「反向说明」，价值不亚于正向说明：

> This is why `RuntimeAgentDef` has **no** `nativeSkillLoading` or `skillInjectionStrategy` field, and why the daemon does **not** create `.cursorrules` or install selected skills into an agent's home directory as part of a run.

技能投递是**共享的 daemon 行为**（第 12 章详述），不是 per-adapter 策略。同理 `docs/agent-adapters.md:376-378`：

> There is **no** public `agents.capabilities()` method and **no** generalized `surgicalEdit`/`streaming`/`resume` feature-gate table.

`capabilities.ts` 全文只有 **131 字节**——它只是一个「从 `--help` 输出里发现的 flag」的内存 map，不是能力模型。

> 🧠 **一句话**：这套契约之所以能撑住 26 条适配器，是因为它极其克制地只描述**「怎么跟这个 CLI 说话」**，而把**「说什么」**（提示词）和**「怎么调度」**（引擎）全部留在共享层。任何想往定义里塞行为的诱惑都被明确拒绝了。

---

<h2 id="ch5">第 5 章 探测与能力协商：为什么必须探测「将来真正会被 spawn 的那个路径」</h2>

### 5.1 探测流水线

`apps/daemon/src/runtimes/detection.ts:238-318` 的 `probe()` 是每条适配器的探测主体：

```mermaid
flowchart TB
    START["probe(def, configuredEnv)"] --> RESOLVE["① resolveAgentLaunch(def, env)<br/>解析出「将来真正会 spawn 的路径」"]
    RESOLVE -->|无路径| UNAVAIL1["不可用 + ExecutableDiagnostic"]
    RESOLVE --> ENV["② 组装 spawn 环境<br/>spawnEnvForAgent + applyAgentLaunchEnv"]
    ENV --> VER["③ 版本探测 probeVersionAtPath"]
    VER -->|OS 级缺失/不可执行| UNAVAIL2["不可用 + NotInvocableDiagnostic"]
    VER -->|能启动但拒绝 --version| OKNOVER["可用，version = null"]
    VER -->|成功| PAR
    OKNOVER --> PAR
    PAR["④ 三个后置探测并发<br/>Promise.all"] --> C1["probeCapabilities<br/>--help 的 flag 表"]
    PAR --> C2["fetchModels<br/>fetchModels() 或 listModels"]
    PAR --> C3["probeAgentAuthStatus<br/>仅当声明了 authProbe"]
    PAR --> C4["probeAmrOpenCodeVersion"]
    C1 --> CACHE["agentCapabilities.set(def.id, caps)"]
    C2 --> OUT["DetectedAgent"]
    C3 --> OUT
    C4 --> OUT
    CACHE --> OUT
```

**① 为什么必须先 `resolveAgentLaunch`？** 这是全章最值得记的一条，`detection.ts:243-250` 的注释是一份完整的现场记录：

> Detection must probe the exact path the runtime will spawn, not just the PATH-visible shim. **This is load-bearing for Codex under nvm/fnm/mise**: the discovered `codex` entry is often a `#!/usr/bin/env node` wrapper that is not invocable from a GUI-launched app's stripped PATH, while the launch resolver can still upgrade it to the packaged native Codex binary. **If detection probes the shim but chat/run spawns the native binary, the UI incorrectly reports "not installed" until the user pins CODEX_BIN by hand even though the real launch path is healthy.**

翻译成人话：GUI 启动的 App 拿到的 PATH 是被系统精简过的，nvm 装的 shim 在里面跑不起来；但启动解析器有能力升级到打包的原生二进制。如果探测用 A 路径、真跑用 B 路径，用户就会看到「没装」但其实能跑——**探测和执行必须走同一条路径解析**，这是一条硬约束。

**② 版本探测的三态判定**（`docs/agent-adapters.md:121-124`）：

| 结果 | 判定 |
|---|---|
| OS 级缺失 / 不可执行 | **不可用** |
| 能启动，但拒绝 `--version` flag | **可用**，version 留空 |
| 成功 | 可用 + version |

第二种情况的容忍度很关键：一些 CLI 的版本 flag 变过名，不该因此判死。

**③ 并发的动机被写进注释**（`detection.ts:273-277`）：

> The version probe must finish first (it gates availability), but the three post-version probes are independent reads — run them concurrently so **a single agent's detection wall is max(help, models, auth) ≈ 5s rather than the sum ≈ 15s.**

**④ 每条适配器故障隔离**（`docs/agent-adapters.md:130`）：「Each adapter probe is fault-isolated so one broken executable cannot empty the whole picker.」`detection.ts:361` 附近的注释还专门提到：没有这个保护，裸 `Promise.all` 会因一条拒绝而整体拒绝。

### 5.2 鉴权：只对声明了探针的适配器断言

这是一处**刻意的减法**。`docs/agent-adapters.md:126-128`：

> Definitions without `authProbe` are **not** assigned a synthetic auth failure from a config-directory guess.

老版本的做法是「看 `~/.foo/` 目录在不在」来猜有没有登录——猜错的代价是把能用的 agent 标成红叉。现在的规则是：**没声明探针就是 unknown**，真正的鉴权失败只从**真实运行失败的错误文本**里推断（`classifyAgentServiceFailure`）。

`types.ts:228-234` 的注释把这个演进说得很清楚：

> This replaces the previous hardcoded "only cursor-agent gets an auth probe" gate: an adapter opts in by declaring a cheap, side-effect-free status/whoami command.

Claude 的探针就是 `['auth', 'status']`，5 秒超时（`claude.ts:26-29`）。

### 5.3 探测的 UX：流式返回

`docs/agent-adapters.md:427-431`：`GET /api/agents?stream=1` 每探完一条就发一个 `agent` SSE 事件，最后 `done`。设置面板因此**不用等最慢的那条 CLI** 就能开始画卡片。非流式版本返回完整数组。

**没有 24 小时探测缓存**（`docs/agent-adapters.md:112-114`）——每次调用都重新并发探测。这是一个「宁可多花几秒也不要给用户看陈旧状态」的取舍。

---

<h2 id="ch6">第 6 章 四种流格式：从 <code>claude-stream-json</code> 到 ACP 再到裸文本</h2>

### 6.1 七种 `streamFormat`，四类解析器

`docs/agent-adapters.md:139-147` 的分组表（对齐 `registry.ts` 的 26 条定义）：

| `streamFormat` | 运行时 id | 解析器 |
|---|---|---|
| `claude-stream-json` | `claude` · `amp` · `codebuddy` | `runtimes/claude-stream.ts`（25.9 KB） |
| `json-event-stream` | `codex` · `cursor-agent` · `opencode` · `mimo` · `byok-opencode` | `runtimes/json-event-stream.ts`（30.2 KB，按 `eventParser` 再分派） |
| `copilot-stream-json` | `copilot` | `src/copilot-stream.ts`（唯一平铺在 `src/` 的解析器） |
| `qoder-stream-json` | `qoder` | `runtimes/qoder-stream.ts` |
| `acp-json-rpc` | `amr`(Vela) · `devin` · `hermes` · `kimi` · `kiro` · `kilo` · `reasonix` · `trae-cli` · `vibe` | `agent-protocol/acp/`（session.ts 40.7 KB） |
| `pi-rpc` | `pi` | `agent-protocol/pi-rpc/` |
| `plain` | `aider` · `antigravity` · `atomcode` · `deepseek` · `grok-build` · `qwen` | `runtimes/plain-stream.ts` |

九条 ACP 适配器共用**同一个传输层**——这是「新增一条 ACP agent 只需一个 def 文件」的直接原因。`docs/agent-adapters.md:27` 那句「A genuinely new wire format is the only case that also adds an engine file」在这里得到了验证：ACP 生态长起来之后，新增 agent 的边际成本降到了一个几百字节的对象字面量（`defs/kiro.ts` 637 字节、`defs/vibe.ts` 635 字节、`defs/kilo.ts` 629 字节）。

### 6.2 `plain` 流的 `<artifact>` 提取：一段值得逐行读的解析器

`runtimes/plain-stream.ts` 全文 473 行，解决的问题是：**裸文本流的 CLI 没有结构化的文件写入工具调用，怎么把它的产出落成项目文件？**

约定是 Anthropic 风格的源码块（`docs/agent-adapters.md:357-362`）：

```html
<artifact identifier="landing-page" type="text/html" title="Landing page">
<!doctype html>
<html>...</html>
</artifact>
```

映射表在 `plain-stream.ts:53-64`：

| `type` | 落成的文件 |
|---|---|
| `text/html` / `html` | `<identifier>.html` |
| `text/css` / `css` | `<identifier>.css` |
| `image/svg+xml` / `svg` | `<identifier>.svg` |
| `text/markdown` / `text/x-markdown` / `markdown` / `md` | `<identifier>.md` |

**但真正精彩的是它避开的四个坑**：

**坑 1 — Markdown 代码围栏里的假 `<artifact>`（`plain-stream.ts:78`、`252-306`）**。模型经常在解释「你应该这样写」时把 `<artifact>` 放进 ```` ``` ```` 围栏或反引号里。天真的 `indexOf('<artifact')` 会把教学示例当成真产物写盘。所以解析前先算出**跳过区间**：

```ts
// plain-stream.ts:252-255
function computeMarkdownSkipRanges(text: string): Array<[number, number]> {
  const fenceRanges = computeMarkdownFenceRanges(text);
  return [...fenceRanges, ...computeMarkdownInlineCodeRanges(text, fenceRanges)];
}
```

围栏的开闭用 `FENCE_OPEN_RE = /^```(\w[\w+-]*)?\s*$/` 和 `FENCE_CLOSE_RE = /^```\s*$/`（`plain-stream.ts:50-51`）逐行判断，行内反引号则要**匹配相同数量的连续反引号**（`countBacktickRun` / `findMatchingBacktickRun`，`plain-stream.ts:349-375`）。注释还标明这份实现**镜像** `apps/web/src/artifacts/markdown-context.ts`——headless 落盘和浏览器解析必须看到同一套围栏边界。

**坑 2 — `<artifacts>` 这样的前缀误判（`plain-stream.ts:230-233`）**：

```ts
function isRealArtifactOpenAt(text: string, idx: number): boolean {
  const next = text.charAt(idx + OPEN_TAG.length);
  return /\s/.test(next);     // '<artifact' 后面必须是空白
}
```

**坑 3 — 属性值里的 `>`（`plain-stream.ts:235-250`）**。`findOpenTagEnd` 是一个带引号状态机，不是 `indexOf('>')`：`title="A > B"` 不会被截断。

**坑 4 — 嵌套 / 未闭合（`plain-stream.ts:85-103`）**。每次找到一个开标签，先探测**下一个**开标签的位置：如果下一个开标签出现在当前开标签结束之前、或出现在闭标签之前，就说明当前这个是坏的，直接跳到下一个重新开始。这样一段畸形输出不会吞掉后面所有合法产物。

另外还有防炸弹的 `MAX_ARTIFACTS_PER_RUN = 50`（`plain-stream.ts:44`）和文件名去重（`reserveUniqueArtifactFileName`，`plain-stream.ts:398-419`：冲突加 `-2`、`-3`，上限 10 000）。

**落盘时同时写清单**（`plain-stream.ts:421-472`）：每个产物按扩展名生成 `artifactManifest`，声明 `kind` / `renderer` / `exports`：

```ts
// .html → { kind:'html', renderer:'html', exports:['html','pdf','zip'], primary:true }
// .css  → { kind:'code-snippet', renderer:'code', exports:['txt','zip'] }
// .svg  → { kind:'svg', renderer:'svg', exports:['svg','zip'] }
// .md   → { kind:'markdown-document', renderer:'markdown', exports:['md','html','pdf','zip'] }
```

这个清单就是 `CONTEXT.md:23-25` 定义的 **Artifact Manifest**——「把一个项目文件标记成正式产物，并记录它的种类、渲染器、导出方式、入口文件」的旁挂元数据。

> 🧠 **一句话**：`plain-stream.ts` 是整个仓库「工程细节密度」最高的文件之一。它证明了一件事——**支持能力最弱的那条适配器，需要写最多的代码**。

### 6.3 嵌套 agent：一个诚实的开放问题

`docs/agent-adapters.md:518-522`：

> Structured streams can carry a child agent's messages inline. For example, Claude Task frames have a non-null `parent_tool_use_id`; the parser surfaces their content but **prevents the child's `turn_end` from completing the parent run**. The UI still does not expose an independent nested-run tree.

也就是说：Claude Code 用 Task 工具开子 agent 时，子 agent 的 `turn_end` 会被识别出来但**不允许它结束父 run**——否则整个回合会提前收尾。内容能显示，但 UI 里没有嵌套树。文档把它明确列在「Open questions」里，而不是假装已解决。

---

<h2 id="ch7">第 7 章 两档执行画像：<code>filesystem</code> vs <code>text_artifact</code></h2>

这是 OD 处理「适配器能力参差」的核心抽象，只有一个 249 字节的文件：

```ts
// packages/contracts/src/execution-profile.ts
export type ExecutionProfile = 'filesystem' | 'text_artifact';
export function executionProfileFromStreamFormat(streamFormat) { … }
```

`streamFormat === 'plain'`（以及 BYOK/API 执行）→ `text_artifact`；其余 → `filesystem`。

### 7.1 两条交付路径

```mermaid
flowchart TB
    RUN["一次生成"] --> PROF{"executionProfileFromStreamFormat<br/>(def.streamFormat)"}

    PROF -->|filesystem| F1["CLI 用自己的 Write/Edit 原生工具<br/>直接在项目 cwd 写文件"]
    F1 --> F2["文件事件推给文件工作区"]
    F2 --> F3["可预览文件 = 交付物"]
    F3 --> F4["助手以普通摘要收尾<br/>❌ 禁止再输出 &lt;artifact&gt; 源码块"]

    PROF -->|text_artifact| T1["模型循环里没有文件系统工具"]
    T1 --> T2["唯一交付形态：一个完整的<br/>&lt;artifact&gt; 源码块"]
    T2 --> T3["run 结束后 daemon 扫 stdout<br/>persistPlainStreamArtifacts()"]
    T3 --> T4["落成项目文件 + Artifact Manifest"]
    T4 --> F3
```

**两档的提示词契约是相反的**，而且都被写死在提示词里：

- `filesystem` 档：`FILESYSTEM_HANDOFF_OVERRIDE`（`apps/daemon/src/prompts/system.ts:549-572`）明确说「**Do not** output generated source code in a `<artifact type="text/html">` block」「**Do not** duplicate file contents in assistant text after writing them to disk」，并且「A filesystem run that emits a source-code `<artifact>` is treated as an **unexpected fallback** by the host.」
- `text_artifact` 档：`API_MODE_OVERRIDE`（`contracts/src/prompts/system.ts:513-517`）反过来说「**No tools are wired through to you.** `TodoWrite`, `Read`, `Write`, `Edit`, `Bash`, `WebFetch` are unavailable」，并且「The override does **NOT** block `<artifact>` blocks — those are how the web UI receives finished HTML in API mode.」

### 7.2 `API_MODE_OVERRIDE` 为什么必须钉在最顶上

`contracts/src/prompts/system.ts:296-307` 的注释是一份完整的 bug 报告：

> API/BYOK mode (`streamFormat === 'plain'`): no tools are wired through to the model, but the discovery layer + base prompt below still tell it to call TodoWrite/Read/Write/Edit/Bash/WebFetch. Without an explicit top-anchored override, **the model invents pseudo-tool markup (`<todo-list>`, `[读取 X]`) instead of producing real progress events — see #313.** Pin this preamble **ABOVE** DISCOVERY_AND_PHILOSOPHY so it beats the discovery layer's own "these override anything later" header.

这里有一个提示词工程的经典博弈：**发现层自己开头就写着「以下规则覆盖后文一切」**。要压过它，唯一干净的办法是把 API 模式覆盖钉在**绝对顶部**。`contracts/src/prompts/system.ts:498-512` 的 JSDoc 把这个理由完整记录了下来——这是我见过对「提示词优先级」最认真的一处工程注释。

同样的逻辑在 daemon 侧原样复制（`daemon/src/prompts/system.ts:920-932`），注释明确要求「Keep the wording **byte-identical** to the contracts copy so both code paths produce the same observable behaviour.」

> ⚠️ **这也是这个架构的一处成本**：daemon 和 contracts 两份提示词组装器必须**逐字节同步**。仓库里至少 6 处注释在提醒这件事（`system.ts:369-375`、`386-388`、`920-927` 等）。这是「同一套语义要同时服务 CLI 执行和 BYOK 执行」付出的税。

---

<h2 id="ch8">第 8 章 输入分流：六个创建面 × 七种 skill mode</h2>

### 8.1 两套刻意不一一对应的分类法

`docs/modes.md:5-11` 开门见山：

> They are intentionally **not** one-to-one. UI tabs describe the workflow a user starts; skill modes describe how the daemon indexes and routes an instruction bundle.

**UI 侧六个创建标签**（真理在 `apps/web/src/components/NewProjectPanel.tsx` 的 `CreateTab` / `MediaSurface`）：

| 标签 | 项目元数据 | 技能路由 | 主要区别 |
|---|---|---|---|
| **Prototype** | `kind: prototype` | 默认 `prototype` 技能，被选中的设计模板替换 | 响应式 web / 移动 / 平板 / 桌面应用界面 |
| **Live Artifact** | `kind: prototype`, `intent: live-artifact` | 具备 live-artifact 能力的 `prototype` 技能 | 高保真、带数据 / 连接器 |
| **Deck** | `kind: deck` | 默认 `deck` 技能，被选中的设计模板替换 | 幻灯导航 + 演示导出 |
| **Template** | `kind: template` | 用户通过 Share 保存的项目模板 | 从用户自己存的模板起步，**不**回落到内置目录 |
| **Media** | `kind: image` / `video` / `audio` | 匹配 mode 的媒体技能 | provider/模型/画幅/时长/音色/提示模板 |
| **Other** | `kind: other` | 不要求技能 | 自由形态 |

**Daemon 侧七种 `od.mode`**：`prototype` · `deck` · `template` · `design-system` · `image` · `video` · `audio`。

两个必须记住的错位：
1. **`design-system` 是合法的 skill mode，但已不是创建标签**（`docs/modes.md:85-92`）——设计系统被提升成独立的产品页面了。
2. **Live Artifact 只是 prototype + intent**（`docs/modes.md:43-48`）——「Code that branches on project kind alone must therefore also check the intent.」这是一条会被踩的隐坑。

### 8.2 「模板替换默认技能」而不是「模板叠加默认技能」

`docs/modes.md:36-41` 讲了一条容易被误解的规则：

> A selected Start from template **replaces** the tab's default skill as the project's primary `skillId`. At run time the daemon resolves that id across the functional-skill and design-template roots and injects the selected template's `SKILL.md`; **it does not automatically compose the default `prototype` skill with the template.**

也就是说：选了 `saas-landing` 模板，就**没有**默认 prototype 技能了。列表分离（两个 API）**不等于**自动组合。这是刻意的——两份都注入会产生互相打架的工作流指令。

### 8.3 HyperFrames：一个不新增后端 kind 的新入口

`CONTEXT.md:39-41` 记录了一个很聪明的产品/工程折中：

> **HyperFrames Composer Surface**: A standalone Home composer media surface shown between Video and Audio for HTML-based motion generation. It submits as `kind: "video"` with `videoModel: "hyperframes-html"` so persisted projects keep the existing video backend shape **while the Home UI still gives HyperFrames its own entry point.**
> _Avoid_: new project kind, separate backend media kind

前端给它独立入口（用户感知上是一等公民），后端复用 `video` kind + 一个 `videoModel` 值（不动 `ProjectKind` 联合类型、不动 SQLite schema、不动已有项目）。`CONTEXT.md` 里每个术语都带一行 **`_Avoid_`**——列出**不许用的同义词**。这份「领域语言词典」本身就是一份值得抄的工程实践。

---

<h2 id="ch9">第 9 章 心脏地带：<code>composeSystemPrompt</code> 的二十层与缓存分区</h2>

如果说别的 Agent 项目的心脏是主循环，OD 的心脏就是这个函数。

- **daemon 版**：`apps/daemon/src/prompts/system.ts:791-1370`（约 580 行函数体，文件 2 075 行）
- **contracts 版（BYOK/API 用）**：`packages/contracts/src/prompts/system.ts:256-496`

### 9.1 输入：`ComposeInput` 的 30+ 个字段

`daemon/src/prompts/system.ts:791-831` 的解构列表就是这层的全部输入面：

```
agentId · includeCodexImagegenOverride · skillBody · skillName · skillMode · skillModes
designSystemBody · designSystemTitle · designSystemUsageMd · designSystemTokensCss
designSystemComponentsManifest · designSystemFixtureHtml · designSystemPullIndex
designSystemImportMode · craftBody · craftSections · memoryBody · memoryHooks
metadata · template · audioVoiceOptions · audioVoiceOptionsError
critique · critiqueBrand · critiqueSkill · pluginBlock · activeStageBlocks
streamFormat · locale · sessionMode · userInstructions · projectInstructions
mediaExecution · byokMediaDefaults · executionProfile · freeformDeckSignal
promptCoreVariant · mediaHintSignal · platformHintSignal
```

### 9.2 装配顺序（classic 变体）

```mermaid
flowchart TB
    A["① PROMPT_INJECTION_RESISTANCE<br/>（钉最前，后文不可覆盖）"] --> B
    B["② API_MODE_OVERRIDE<br/>仅 streamFormat==='plain'"] --> C
    C["③ PLAN_MODE / CHAT_MODE 覆盖<br/>按 sessionMode"] --> D
    D["④ examplePrompt / skipDiscoveryBrief 覆盖"] --> E
    E["⑤ UI locale 提示"] --> F
    F["⑥ DISCOVERY_AND_PHILOSOPHY（~3000 token）<br/>媒体面 & Ask 模式跳过"] --> G
    G["⑦ 方向库 renderDirectionSpecBlock()<br/>仅当没有活跃设计系统"] --> H
    H["⑧ 共享设备边框目录<br/>仅当多目标项目"] --> I
    I["⑨ 身份与工作流宪章<br/>renderOfficialDesignerPrompt()"] --> J
    J["⑩ 会话中澄清问题（question-form）"] --> K
    K["⑪ 个人记忆 + 意图网关 + 自检记分卡 + 规则提案"] --> L
    L["⑫ 用户级自定义指令"] --> M
    M["⑬ 项目级自定义指令（覆盖用户级）"] --> N
    N["⑭ ## Active design system<br/>USAGE.md → DESIGN.md → import mode →<br/>tokens.css → 组件清单 → pull 索引"] --> O
    O["⑮ craft 工艺规则（按 requires 订阅）"] --> P
    P["⑯ ## Active skill + derivePreflight"] --> Q
    Q["⑰ 语义化输出文件名规则"] --> R
    R["⑱ 插件块 + Active stage 块"] --> S
    S["⑲ 项目元数据块 renderMetadataBlock()"] --> T
    T["⑳ 尾部覆盖：DECK_FRAMEWORK / MEDIA_CONTRACT /<br/>ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE"]
```

注意 **⑭ 和 ⑳ 是同一件事说两遍**：设计系统的内容在第 14 层注入，第 20 层再补一个「**别再问用户要视觉方向了**」的覆盖。为什么要重复？因为第 6 层的发现层里有一条硬规则是「turn 1 必须发问卷」，而问卷里通常有品牌/方向问题。第 20 层的 `ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE`（`daemon/src/prompts/system.ts:603-616`）就是专门去压它的：

```
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
- Do not emit a direction question-form, a `direction-cards` picker, or any visual-direction card while an active design system is present.
- If an earlier discovery answer asks to "Pick a direction for me", treat that as already satisfied by the active design system and continue with the plan.
```

**这就是提示词工程的真实形态**：不是写一段漂亮的指令，而是**在几千行互相冲突的规则之间管理优先级**。

### 9.3 slim vs classic：一次带 A/B 的提示词重构

`promptCoreVariant === 'slim'` 走另一条完全不同的装配路径（`daemon/src/prompts/system.ts:832-907`）。注释解释了两者的差别：

> Slim core collapses the discovery layer + designer charter + their tail overrides into **one charter document**; the classic stack keeps the legacy layered composition **until the A/B comparison signs off.**

这是一个还在验证中的重构：把「发现层 + 设计师宪章 + 尾部覆盖」这三块塌缩成一份宪章文档。两套并存，靠 A/B 决定。

slim 还带了两个 classic 没有的优化：

**① 方向库改成索引 + 按需拉取（`system.ts:996-1005`）**：

> Slim carries only the id+label index and the agent pulls the chosen direction's full spec via `od tools directions --id <id>` — **but ONLY on filesystem runs**. `text_artifact` runs (BYOK/plain adapters) have no tools to dereference the index, so they keep the full inline library like classic; **anything less tells them to bind palettes they cannot fetch.**

一个只有在「有工具」的执行画像下才成立的优化——OD 很小心地做了条件分支。

**② 平台契约块按信号稳定性分区（`system.ts:1023-1037`）**：

> **Trigger stability decides position.** Metadata is fixed at project creation → the block can sit here in the project-stable zone. The conversation-text signal is turn-variable (a mid-session "make it an iOS app" flips it on), so signal-only triggers defer the block to the turn-variable suffix like the deck/media signals — **an early insert would break the cached prefix for every section after this line.**

### 9.4 缓存友好的分区排序：全仓库最精妙的一处工程

`daemon/src/prompts/system.ts:851-867` 的注释是本章的高潮：

> Head ordering differs by variant, **following prompt-caching prefix rules (stable content first)**:
> - classic: injection resistance FIRST so no later section can override it, then mode overrides, then the layered discovery/charter stack.
> - slim (non-ask): **the STATIC charter opens the document** (it embeds the security section right after Precedence), so **every conversation shares the same cacheable prefix**; conversation-stable overrides (mode, locale) follow, project context after that, **turn-variable blocks last**.

把提示词按**变化频率**分成四带：

```mermaid
flowchart LR
    Z1["① 全局静态<br/>设计师宪章 · 注入抵抗<br/>所有会话共享"] --> Z2["② 会话稳定<br/>模式覆盖 · locale"]
    Z2 --> Z3["③ 项目稳定<br/>设计系统 · 技能 · 元数据 · 平台"]
    Z3 --> Z4["④ 回合可变<br/>deck/media/platform 信号触发块"]

    style Z1 fill:#e8f5e9
    style Z2 fill:#f1f8e9
    style Z3 fill:#fff9e6
    style Z4 fill:#ffebee
```

**为什么这样排？** 因为 LLM 的前缀缓存是**前缀匹配**的：只要前 N 个 token 一样就能命中。把最稳定的放最前，一个回合中途翻转的信号（用户说「改成 iOS 版」）只会作废**缓存后缀**，而不是整份提示词。

配套还有一个 `describeStablePromptCache()`（`runtimes/chat-prompt-inputs.ts:394-442`）做**缓存命中归因**：

```ts
if (!isResuming)                              → missReason: 'new-session'
if (storedStablePromptHash === currentHash)   → hit: true
if (storedStablePromptHash === null)          → missReason: 'missing-stored-hash'
else                                          → missReason: 'stable-prompt-changed'
                                                + changedSections: 逐段 diff
```

最后那个 `changedSections` 只在**真正漂移**时计算。注释（`chat-prompt-inputs.ts:435-437`）解释：`missing-stored-hash` 是没有基线的老行，在那里报告「所有段都变了」会淹没真正关心的信号。

> 🧠 **一句话**：hermes-agent 的绝活是「前缀缓存神圣」，OD 把同一个理念推进了一步——**不只是不破坏前缀，还给每一段打上「变化频率」标签，按频率排序，并且监控每次未命中是哪一段造成的**。这是我在这十个项目里看到的最成熟的提示词缓存工程。

### 9.5 Ask 模式：轻，但不失忆

`sessionMode === 'chat'` 时（`daemon/src/prompts/system.ts:939-947`）：

> the CHAT_MODE_OVERRIDE below IS the whole charter, and every artifact-oriented block (**the ~3k-token discovery layer, direction library, device frames, the full designer charter, deck framework, media contracts, codex imagegen override, critique panel, DS visual-direction override**) is gated off so the turn stays cheap. Memory, custom instructions, the active design system, attached skills, plugins, MCP tools, and the clarifying-questions surface are still composed in — **Ask mode is light, not amnesiac.**

省掉的是**工作流**，保留的是**上下文**。这个区分很关键：用户问「这个配色为什么这么选」时，不需要设计师宪章，但需要知道当前设计系统是什么。

---

<h2 id="ch10">第 10 章 三条硬规则：question-form → 品牌分支 → TodoWrite</h2>

`DISCOVERY_AND_PHILOSOPHY`（`packages/contracts/src/prompts/discovery.ts:25-354`，330 行纯提示词）是整个产品的**行为脚本**。它蒸馏自两个上游项目（`discovery.ts:18-21`）：`alchaincyf/huashu-design`（初级设计师模式、变体而非答案、反 AI 味、化身专家）和 `op7418/guizang-ppt-skill`（预飞资源读取、P0 自检、主题节奏）。

### 10.1 三条规则的时序

```mermaid
sequenceDiagram
    participant U as 用户
    participant A as Agent
    participant H as OD 宿主

    Note over A: RULE 1 · 第 1 回合
    U->>A: "帮我做一个 SaaS 落地页"
    A->>H: 一句短散文 + <question-form id="discovery"> + 停
    Note right of A: ❌ 不读文件 ❌ 不 Bash<br/>❌ 不 TodoWrite ❌ 不扩展思考
    H->>U: 渲染成问卷卡片（预填了推荐值）

    Note over A: RULE 2 · 第 2 回合
    U->>A: "[form answers — discovery] brand: brand_spec …"
    alt 分支 A：用户给了品牌/参考源
        A->>A: 五步品牌提取（Bash/Read/WebFetch）
        A->>H: 写 brand-spec.md（6 个 OKLch 色 token + 字体栈 + 3-5 条版式姿态）
        A->>U: 一句话复述系统（便于廉价纠偏）
    else 分支 B：没有品牌源
        A->>A: 直接用活跃设计系统 / 自己从方向库挑一个
        Note right of A: ❌ 绝不再弹第二个方向问卷
    end

    Note over A: RULE 3 · 第 3 回合起
    A->>H: TodoWrite 计划（9 步模板）
    loop 每完成一步
        A->>H: 立刻标 completed，下一步标 in_progress
    end
    A->>A: 第 7 步 references/checklist.md（P0 必须全过）
    A->>A: 第 8 步 五维自评（哲学/层次/执行/具体性/克制），<3/5 就返工
    A->>H: 第 9 步 交付（filesystem 档写文件；text_artifact 档发 <artifact>）
```

### 10.2 RULE 1 的偏执程度

`discovery.ts:40-44`：

> When the user opens a new project or sends a fresh design brief, your **very first output** is one short prose line + a `<question-form>` block. **Nothing else. No file reads. No Bash. No TodoWrite. No native tool calls. No extended thinking.** The form is your **time-to-first-byte**.

「问卷就是你的首字节时间」——这句话点破了整条规则的产品动机：**用户容忍不了 15 秒的沉默**，但完全能接受 2 秒内弹出一张能一路点完的表单。

而且它明确堵死了模型最爱找的借口（`discovery.ts:154`）：

> The form **applies** even when the user's brief looks complete. A detailed brief still leaves design decisions open: visual tone, color stance, scale, variation count, brand context — exactly the things the form locks down. **Do not justify skipping it ("the brief is rich enough"); ask anyway.** The user is fast at picking radios; they are slow at re-doing a wrong direction.

只有三种情况允许跳过（`discovery.ts:156-159`）：在已有设计里做微调、用户明说「skip questions / just build」、消息以 `[form answers — …]` 开头。

### 10.3 表单编写规则里的 12 条工程细节

`discovery.ts:135-152` 那段「Form authoring rules」信息密度极高，挑几条最有代表性的：

**① 硬上限 5 题（`discovery.ts:150`）**：

> **Hard cap: 5 questions per form — never more.** Before emitting, count the questions in your draft; if there are more than 5, delete the least build-critical until exactly 5 or fewer remain. **A question earns its place only if its answer genuinely changes what you would build for THIS brief.**

**② 预填是强制的，且 `default` 必须写在 `options` 前面（`discovery.ts:143`）**：

> Place the `default` key **before** `options` in each question object … **the host renders forms token-by-token, and a `default` that trails a long `options` array reaches the user late.**

这是一条**因为流式渲染而产生的 JSON 键顺序要求**——纯粹的工程细节泄漏进提示词，但完全合理。

**③ 分支值必须保持英文（`discovery.ts:144`）**：

> Localize every user-facing string … **write what a native speaker would naturally say, never a word-for-word translation (the Chinese title is 快速确认 · 30秒, not the literal 快速简报)**. Set the top-level `"lang"` field to the BCP-47 tag … `id`, `type`, option `value`, and the stable branch values (`pick_direction`, `brand_spec`, `reference_match`) **MUST stay in English because later branch rules match against them.**

**显示层全本地化、控制层全英文**——因为 RULE 2 要按 `value` 做分支匹配。而且连「要说人话不要直译」都写进去了。

**④ 不要自己写「其他」选项（`discovery.ts:142`）**：

> The host automatically renders a localized "Other" escape hatch … **do NOT author your own catch-all "Other …" option; it would duplicate the host's.**

**⑤ 元数据与插件输入等价（`discovery.ts:149`）**：这一条最长，核心是「Project metadata」和「Plugin inputs」两个来源**同等权威**，任一来源提供了答案就删掉对应的默认问题，标了「(unknown — ask)」的字段才要新增问题。它甚至列出了同义字段映射：`platform` / `surface` / `platformTargets` / `target` 都答「目标平台」；`slideCount` / `slides` / `pageCount` 都答「页数」。

### 10.4 RULE 2 的分支解析顺序

`discovery.ts:167-172` 定义了一个**四步优先级**：

```
1. 当前消息/附件/先前简报/URL 里已经有真实品牌源  → 分支 A
2. 否则看提交的 brand 值（有 [value: ...] 就用稳定值，不用可见标签）
3. brand 值是 "brand_spec" 或 "reference_match"    → 分支 A
4. 否则                                             → 分支 B
```

**分支 A 的五步品牌提取**（`discovery.ts:176-187`）每步一次工具调用：

1. **定位源**：附件就列出来；URL 就 WebFetch `<brand>.com/brand`、`/press`、`/about`
2. **下载样式产物**：CSS、品牌指南 PDF、截图
3. **提取真值**：`grep -E '#[0-9a-fA-F]{3,8}'` 抓 CSS 里的 hex；截图靠肉眼看排版。**「Never guess colors from memory.」**
4. **编码成契约**：写 `brand-spec.md`，含六个 OKLch 色 token（`--bg` `--surface` `--fg` `--muted` `--border` `--accent`）、display/body/mono 字体栈、3–5 条观察到的版式姿态（圆角、边框粗细、accent 预算）
5. **口头复述**：一句话说清将用的系统，让用户能廉价纠偏

有一条防幻觉的硬规则（`discovery.ts:178`）：选了 `brand_spec` / `reference_match` 但**还没给源**，就**要源并停下**——「**Do not guess a brand domain or invent tokens.**」

**分支 B 明确禁止二次问方向**（`discovery.ts:191-193`）：

> Skip directly to RULE 3. **Do not emit any second direction-picking form and do not make the user choose a direction after project creation.** … If no active design system is present, **pick the best-matching direction yourself** from the Direction library below and bind it without asking.

这是一次产品决策的沉淀：早期版本大概会弹「五选一方向卡」，后来发现多一次点击就多一次流失，改成「自己选，用户不满意再说」。

### 10.5 RULE 3 的九步计划模板与两道非协商闸门

`discovery.ts:209-221` 的标准计划：

```
1. 读活跃 DESIGN.md + 技能资源（template.html, layouts.md, checklist.md）
2. （分支A）确认 brand-spec.md 并绑进 :root
   （有活跃DESIGN.md）绑定设计系统 token/规则到 :root
   （否则）自己挑一个匹配调性的方向，绑进 :root
3. 规划章节/幻灯/屏幕清单，含平台变体与节奏（写之前先口头说一遍清单）
4. 把种子模板拷到项目根
5. 粘贴并填充规划好的版式/屏幕/幻灯
6. 用简报里的真实、具体文案替换 [REPLACE] 占位
7. 自检：跑 references/checklist.md（P0 必须全过）
8. 评审：五维雷达（哲学/层次/执行/具体性/克制），任一 <3/5 就修
9. 若本回合写了新的规范 HTML 就发单个 <artifact>；否则总结改动
```

`discovery.ts:227`：「**Step 7 (checklist) and step 8 (critique) are non-negotiable.**」

**五个维度**（`discovery.ts:235-243`）值得原样记住，因为它是后面 Design Jury 的人类可读版：

| 维度 | 拷问 |
|---|---|
| **Philosophy** 哲学 | 视觉姿态和要求的匹配吗（editorial vs minimal vs brutalist）？还是漂回了你最爱的默认？ |
| **Hierarchy** 层次 | 每屏眼睛有一个明显落点吗？还是所有元素在互相竞争？ |
| **Execution** 执行 | 排版、间距、对齐、对比——是对的，还是只是「差不多」？ |
| **Specificity** 具体性 | 每个词、数字、图片都是**这个**简报专属的吗？还是混进了填充和通用数据味？ |
| **Restraint** 克制 | 一个 accent 最多用两次、一个决定性亮点——还是三个亮点在打架？ |

「Any dimension under 3/5 is a regression. Go back, fix the weakest, re-score. **Two passes is normal.**」——注意最后那句：**默认就该返工两轮**，这是把「一次成型」这个不现实的期待从流程里拿掉了。

### 10.6 Deck 的「框架优先」铁律

`discovery.ts:223` 单独拎出来讲幻灯：

> **Decks especially — framework first, content second.** For `kind=deck` projects, step 4 is the load-bearing one: copy the deck framework HTML … **verbatim** before authoring any slide content. **Do NOT write your own scale-to-fit logic, keyboard handler, slide visibility toggle, counter, or print stylesheet — every freeform attempt at this re-introduces the same iframe positioning / scaling bugs we have already fixed in the framework.**

组装器里有对应的实现（`contracts/src/prompts/system.ts:466-485`）：

```ts
const isDeckProject     = skillMode === 'deck' || metadata?.kind === 'deck';
const isFreeformProject = !skillMode && (!metadata || metadata.kind === 'other');
const hasSkillSeed      = !!skillBody && /assets\/template\.html/.test(skillBody);
if (!isAskMode && isDeckProject && !hasSkillSeed) {
  parts.push(DECK_FRAMEWORK_DIRECTIVE);
} else if (!isAskMode && isFreeformProject && !hasSkillSeed) {
  parts.push(`## If this brief is a slide deck / keynote / presentation … ${DECK_FRAMEWORK_DIRECTIVE}`);
}
```

三个判断的注释（`system.ts:450-465`）解释了每个分支的来历：
- `metadata.kind === 'deck'` 这条是为了兜住**没绑技能的 deck 项目**——否则「既没有技能种子也没有框架骨架，agent 从零写缩放/导航/打印逻辑，又踩同一个 `place-items: center` + transform 的坑」。
- **技能种子优先**：种子自己定义了更有主张的框架（simple-deck 的 scroll-snap、guizang-ppt 的杂志版式），再钉通用骨架会冲突。
- **自由形态项目**（用户跳过了 kind 选择）单独走一条带条件前缀的分支：「**If — and only if — the brief reads as slides, keynote, presentation, deck, PPT, or 讲解**, follow the framework below. Otherwise ignore everything in this section.」

这三个分支就是三次线上事故的化石。

---

<h2 id="ch11">第 11 章 记忆与双环：task-brief / verify-scorecard / rule-proposal</h2>

### 11.1 记忆不是硬规则

记忆块的前言（`contracts/src/prompts/system.ts:378`）定义了一套**冲突仲裁**：

> Treat them as **preferences and context, NOT hard rules**: when they collide with the active design system tokens, **the brand wins**; when they collide with the active skill's workflow, **the skill wins**. They are still authoritative for **tone, voice, terminology**, and what the user already told you about themselves and their goals — **never re-ask the user about something already captured here.**

注释（`system.ts:369-375`）说明了这段措辞的用意：「**what stops the model from treating remembered preferences as harder than the active design system**」。也就是防止「用户上次说喜欢深色」压过「本次品牌是浅色」。

### 11.2 记忆作为「任务意图网关」

`contracts/src/prompts/system.ts:378` 后半段：

> Use memory as a **task-intent gateway**. When the user's request is short or underspecified, **silently expand it into an internal task brief before acting**: infer the task type, user/profile background, project/artifact context, delivery preferences, known feedback meanings, constraints, and validation/finish line. … **Expanding intent this way changes only WHAT you know going in; it never shortcuts the standard build flow** — you still plan with TodoWrite and still run the anti-slop / brand self-check on every artifact-producing turn.

### 11.3 三张 `<od-card>`：把内部状态变成可交互 UI

OD 定义了一套 `<od-card type="…">` 助手文本块，宿主渲染成卡片。三种类型对应记忆的三个环：

| 卡片 | 何时发 | 由什么开关控制 | 作用 |
|---|---|---|---|
| `task-brief` | **PRE**：短请求被记忆扩写成完整简报时，回复最开头发一张折叠卡 | `memoryHooks.rewrite`（缺省 ON） | 让用户看见「我理解成了什么」，可立刻纠偏 |
| `verify-scorecard` | **POST**：产出/编辑产物后，逐条核对「已验证规则」 | `memoryHooks.verify`（缺省 ON） | daemon **程序化检查**这张卡是否存在 |
| `rule-proposal` | 用户的纠正隐含一条可复用、可检查的规则时 | 常开 | **提案而非静默保存**，用户点 Keep 才生效 |

三个都有严格的行为约束：

**`task-brief`（`system.ts:391`）**：

> Emit **at most one** task-brief per turn. Skip it entirely when the request is already explicit or trivial. … **Never dump the brief as prose — only as the card.**
> The task-brief card **REPLACES the turn-1 discovery question-form** when memory already makes the intent clear — **it does NOT replace the rest of the build flow.** … **Skipping the discovery form when intent is already understood is correct; skipping TodoWrite or the anti-slop gate is not.**

**`verify-scorecard`（`system.ts:397`）**——这里出现了**程序化执法**：

> The daemon **programmatically checks this scorecard after your turn** — a missing scorecard or a rule left uncovered on an artifact turn is **recorded as an enforcement failure** — so always emit it when verified rules apply.

并且规定了收尾顺序：

> Order the end of your turn as: **(1)** finish the anti-slop / brand self-check and fix any failure in place, **(2)** emit the verify-scorecard card, **(3)** close with the normal handoff.

以及「Prefer fixing silently over asking. Leave a row as `fail` only when fixing it needs a decision you genuinely cannot make.」——**能自己修就别问**。

**`rule-proposal`（`system.ts:402`）**——最克制的一条：

> Propose **at most one rule per turn**, and only when confident it generalizes beyond the current artifact. **Do not claim in prose that a rule was recorded, saved, noted, added to memory, or will be remembered unless this same response includes the rule-proposal card for that rule; the rule becomes saved only after the user clicks Keep.**

最后半句在治一个**具体的模型撒谎行为**：模型很爱说「好的，我记住了」，但实际上什么也没记。OD 的解法是把「记住了」这个断言**绑定到卡片的存在性**上。

```mermaid
flowchart TB
    IN["用户短请求"] --> MEM{"记忆够不够<br/>扩写成简报？"}
    MEM -->|够 & rewrite=ON| CARD1["① &lt;od-card type='task-brief'&gt;<br/>替代 turn-1 问卷"]
    MEM -->|不够| FORM["走 RULE 1 问卷"]
    CARD1 --> BUILD["TodoWrite + 构建<br/>（不可跳过）"]
    FORM --> BUILD
    BUILD --> SLOP["反 AI 味 / 品牌自检<br/>（不可跳过）"]
    SLOP -->|verify=ON & 有已验证规则| CARD2["② &lt;od-card type='verify-scorecard'&gt;<br/>daemon 程序化检查其存在"]
    SLOP --> HANDOFF["交付收尾"]
    CARD2 --> HANDOFF
    HANDOFF --> FB{"用户纠正里<br/>隐含可复用规则？"}
    FB -->|是| CARD3["③ &lt;od-card type='rule-proposal'&gt;<br/>Keep / Edit / Discard"]
    CARD3 -->|用户点 Keep| STORE[("已验证规则库")]
    STORE -.->|下次注入| MEM
```

> 🧠 **一句话**：OD 的记忆系统跟 openworker 的「显式 SQLite 事实」和 Raven 的「EverOS 双轨」都不同——它是**带 UI 的、可审计的、需要用户点确认才落库的**。三张卡把「模型的内部状态」变成了「用户能看见、能改、能拒绝的界面元素」，这是我在这十个项目里见过最产品化的记忆设计。

---

<h2 id="ch12">第 12 章 内容四平面：skills / design-templates / design-systems / craft</h2>

### 12.1 技能格式：原样吃下 Claude Code 的 `SKILL.md`

`docs/skills-protocol.md:13-51`：基础格式**完全不改**Claude Code 的约定，OD 只在 frontmatter 里加一个可选的 `od:` 命名空间。`docs/skills-protocol.md:121` 的目标写得很明确：

> The goal: **zero-config compatibility** for existing Claude Code skills.

一个技能什么 `od:` 都不写会怎样（`docs/skills-protocol.md:112-120`）？

| 字段 | 缺省推断 |
|---|---|
| `mode` | 从 description 和正文推断，兜底 `prototype` |
| `surface` | image/video/audio 条目取对应媒体，否则 `web` |
| `preview.type` | `html` |
| `design_system.requires` | **`true`**（要显式写 `false` 才不给品牌上下文） |
| `example_prompt` | description 的第一句，截断到选择器长度 |
| `scenario` | 从 description/正文推断，兜底 `general` |

### 12.2 `.od-skills` 暂存：从符号链接到真实拷贝的一次安全修复

这是本章最值得学的一段。`apps/daemon/src/cwd-aliases.ts:1-30` 的文件头注释是一份完整的设计说明 + 安全尸检：

> **Why a per-project copy and not a symlink/junction**
> An earlier draft of this fix (PR #435 round 1) created a directory link pointing at the repository's live `skills/` tree. **Reviewers flagged that as a write-amplification vulnerability: agents have write access to their cwd, and a `Write`/`Edit`/`Bash` call against `.od-skills/<id>/SKILL.md` resolves through the symlink and mutates the shipped resource itself.** Per-project copies eliminate that channel — every byte under `.od-skills/` is a private working copy, and corrupting it has no effect on other projects or on the source.

也就是说：**agent 对 cwd 有写权限**，如果 `.od-skills/` 是指向仓库 `skills/` 的软链，一次 `Edit` 就能改坏所有项目共用的技能源文件。这是一个非常真实的**写放大攻击面**。

三个配套细节：

**① 成本论证（`cwd-aliases.ts:20-24`）**：只暂存**活跃**技能而非整个 `SKILLS_DIR`；单个技能通常 1–3 MB；APFS / btrfs / ReFS 上 `fs.cp` 走 CoW，稳态成本只是几个 syscall。

**② `dereference: true`（`cwd-aliases.ts:25-30`）**：

> We `dereference: true` so the staged copy is fully self-contained — **nothing inside it can write back to a real file outside the project.** We also call `stat()` (not `lstat()`) on the source root so an environment that puts `skills/` itself behind a symlink (e.g. a content-addressable mount) is followed correctly.

**③ 别名带路径哈希（`cwd-aliases.ts:50-56`）**：

```ts
export function skillCwdAliasSegment(dir: string): string {
  const folder = path.basename(dir) || 'skill';
  const normalizedDir = path.resolve(dir).replaceAll('\\', '/');
  const digest = createHash('sha256').update(normalizedDir).digest('hex').slice(0, 10);
  return `${folder}-${digest}`;    // 如 saas-landing-3f2a91b0c4
}
```

为什么要哈希？因为用户根和内置根可能有**同名技能**（遮蔽关系），两者都被选中时目录名会撞。哈希源路径就解决了。

**④ 跨文件系统兜底**：`fs.cp` 底层的 `copy_file_range(2)` 在跨文件系统时会被拒（常见 `EXDEV`；容器镜像层拷到 ZFS/overlay bind mount 上会是 `EPERM`）。Node 不会自动降级，所以 OD 自己实现了**递归流式拷贝兜底**（`docs/agent-adapters.md:170-171` 也提到了这一点）。

**⑤ 提示词里给两条路径**（`docs/agent-adapters.md:174-176`）：cwd 相对的暂存路径（主）+ 绝对源路径（兜底），这样**暂存失败时 agent 仍能工作**。

```mermaid
flowchart LR
    SRC1["内置 skills/saas-landing/"] -->|fs.cp dereference:true| STAGE
    SRC2["用户根 skills/saas-landing/"] -->|同名遮蔽| SRC1
    STAGE[".od-skills/saas-landing-3f2a91b0c4/<br/>（项目私有真实拷贝）"]
    STAGE --> AGENT["Agent 读 SKILL.md / assets/ / references/"]
    AGENT -.->|即使 Edit 写坏了| STAGE
    STAGE -.->|❌ 不会影响| SRC1
    FAIL["暂存失败"] -.->|提示词里的绝对路径兜底| SRC1
```

### 12.3 设计系统包：三文件最小契约 + 六层组装顺序

**包形状**（`design-systems/README.md:8-22`）：

```
design-systems/<slug>/
├── manifest.json    ← 发现元数据、来源出处、声明的包内路径
├── DESIGN.md        ← 给 agent 看的规范散文（canonical）
└── tokens.css       ← 编译好的语义 token 样式表（canonical）
```

约束（`design-systems/README.md:44-49`）：文件夹 slug 必须等于 `manifest.id` 且用规范化 ASCII；`files.design` 固定是 `DESIGN.md`、`files.tokens` 固定是 `tokens.css`；**每条声明的路径必须安全、相对、存在**。

**富文件是缓存不是真理源**（`design-systems/README.md:79-86`）：

```
components.manifest.json  ← 由 components.html + tokens.css 派生
design-tokens.json        ← 由 token 契约报告派生，必须与 tokens.css 一致
tailwind-v4.css           ← 由 tokens.css 派生
```

**注入顺序**（`docs/skills-protocol.md:200-213`）——这是设计系统真正进提示词的方式：

```
1. 包专属 USAGE.md 指引（或默认使用契约）
2. 完整的 DESIGN.md 正文
3. import-mode 指引（声明了才有）
4. tokens.css
5. 紧凑的组件清单，或在无法产出清单时用 components.html
6. manifest 派生的「富文件按需拉取索引」
7. craft 工艺规则
8. 活跃技能/模板正文
```

`docs/skills-protocol.md:215-218` 特别澄清了三件**不做**的事：

> Design systems are **not** copied into the run CWD, **not** section-pruned through `od.design_system.sections`, and **not** substituted through a `{{ design_system }}` variable.

默认使用契约（`daemon/src/prompts/system.ts:617`）值得原样引用：

> Read DESIGN.md for visual principles, **paste tokens.css verbatim into the first `<style>`** when it is provided, and match component shapes from the reference component manifest or fixture when available. Treat any pull-layer index as **optional** context for deeper inspection; **do not assume those files have already been loaded.**

最后半句在防一个具体的幻觉：模型看到索引就以为文件已经读过了。

**七个 H2 的下限，但不规定标题**（`design-systems/README.md:103-107`）：

> `DESIGN.md` does **not** use a fixed nine-section template. The package-quality guard requires **at least seven substantive H2 headings** for migrated packages, **without prescribing their names, order, or numbering.**

这是从上游 `awesome-design-md` 的九段式模板演进出来的：保留「必须足够充实」的门槛，去掉「必须叫这几个名字」的僵化。

### 12.4 craft 的两级执法

`craft/README.md:63-70` 定义了一个诚实的双层契约：

| 层级 | 含义 | 例子 |
|---|---|---|
| **Auto-checked** | 接进了 `apps/daemon/src/lint-artifact.ts` 的规则 | `anti-ai-slop.md` 的 P0 列表 |
| **Guidance** | 其余部分：agent 读、评审者用、linter 不查 | `state-coverage.md`、`animation-discipline.md` |

而且 craft 文件里**逐条标注**了哪些是「(guidance, not auto-checked)」（见 `craft/anti-ai-slop.md` 的 P1/P2 小节）。`craft/README.md:70`：「A purely behavioral craft file … is guidance **unless a specific rule is later promoted into `lint-artifact.ts`.`**」

**运行时宽容 vs 仓库严格**（`craft/README.md:53-61`）：

> The loader remains tolerant because externally installed or older bundles may reference a section that is unavailable in the current resource set. **A missing optional paragraph must not make an otherwise usable runtime bundle fail.**
> Checked-in content has a stronger contract: `pnpm lint:craft` and `pnpm guard` **fail** on malformed slugs and unresolved references. A deliberate forward reference is valid **only when it is listed in `craft/FUTURE_SECTIONS.md`.**

这是一条很好的「兼容性 vs 质量」分界线：**运行时不因为缺一段文档而崩，但仓库不许你写错 slug**。而且「故意的前向引用」有一个显式登记处，让「计划中的工作」变成可见的，而不是靠 typo 悄悄漏掉一段提示词。

---

<h2 id="ch13">第 13 章 质量闸门一：<code>lint-artifact</code> 程序化反 AI 味</h2>

`apps/daemon/src/lint-artifact.ts`，**1 000 行 / 46 KB**。这是本仓库最有创造性的一个模块：**把「一眼假」这种主观判断，落成可执行的正则 + CSS 求值**。

### 13.1 规则全表

`lintArtifact(rawHtml)`（`lint-artifact.ts:120-510`）返回 `LintFinding[]`，每条含 `severity` / `id` / `message` / `fix` / `snippet`：

| 严重度 | id | 检查什么 | 行号 |
|---|---|---|---|
| **P0** | `purple-gradient` | `linear-gradient()` 里出现 20 个 Tailwind violet/indigo hex 之一，或字面量 `purple`/`violet` | `131-160` |
| **P0** | `trust-gradient` | 蓝→青「信任渐变」：蓝色系 13 个 hex × 青色系 8 个 hex 配对 | `177`, `559-635` |
| **P0** | `ai-default-indigo` | 7 个「默认 LLM accent」hex 的**纯色**使用（不只渐变） | `205` |
| **P0** | `emoji-icon` | 17 个 slop emoji 出现在 `<h*>` / `<button>` / `<li>` / `class*="icon"` 里 | `228` |
| **P0** | `left-accent-card` | 圆角卡片 + 左侧彩色边框（经典「AI 仪表盘瓦片」形状） | `245` |
| **P0** | `sans-display` | h1/h2/h3 的 `font-family` 落在 Inter/Roboto/Arial/`-apple-system`/`system-ui`/SF Pro | `259` |
| **P0** | `invented-metric` | 「10× faster」「99.9% uptime」「3× more productive」等 5 个句式 | `272` |
| **P0** | `filler-copy` | `lorem ipsum` / `feature one\|two\|three` / `placeholder text` / `sample content` | `287` |
| **P0** | `scroll-into-view` | 用了 `Element.scrollIntoView()` | `296-304` |
| **P0** | `slide-theme-missing` | deck 里有 `.slide` 但缺 light/dark/hero 主题类 | `465-479` |
| **P1** | `all-caps-no-tracking` | `text-transform: uppercase` 但字距 <0.06em（含内联 style） | `306-387` |
| **P1** | `external-image` | unsplash / placehold.co / placekitten / picsum / loremflickr 等外链占位图 | `389-403` |
| **P1** | `raw-hex` | 第一个 `<style>` 里 `:root{}` 之外的裸 hex **超过 12 个** | `405-429` |
| **P1** | `accent-overuse` | `var(--accent)` 在 `<style>` 之外的 body 里出现 **>6 次** | `431-446` |
| **P1** | `slide-rhythm` | 连续 3 张同主题幻灯（视觉疲劳） | `480-506` |
| **P2** | `missing-section-anchor` | `<section>` 缺 `data-od-id` 或 `data-screen-label` | `448-463` |

### 13.2 三处真正有工程含量的检查

**① ALL CAPS 字距：token 感知 + 主题解析 + 字号折算**

天真实现是 `/letter-spacing:\s*([\d.]+)em/` 抓个数字。OD 做的事复杂得多（`lint-artifact.ts:306-358`、`636-720`、`805-892`）：

1. `extractCssTokens(html)` 先把**每个作用域**的 `--name: value` 声明收集起来（`lint-artifact.ts:805-843`）
2. `buildResolvedThemes(scopes)` 把「全局主题作用域」（`:root`、`[data-theme=…]` 等，见 `GLOBAL_THEME_ATTRIBUTES`，`lint-artifact.ts:974-979`）组合成多套主题
3. `resolveCssVars(body, tokens)` 递归解析 `var(--x)`，**最大深度 4**（`VAR_RESOLVE_MAX_DEPTH = 4`，`lint-artifact.ts:844`）
4. `resolveFontSizePx(decls)` 把同规则里的 `font-size` 折算成 px（`ROOT_FONT_PX = 16`），因为 `letter-spacing: 1px` 在 12px 字上够、在 48px 字上远远不够

所以 `letter-spacing: var(--caps-tracking)` 会被**按每套主题里那个 token 的字面值**判定，而不是当成「没设置」。

**② 正则的一个真实教训（`lint-artifact.ts:328-341`）**

那段注释值得完整引用，因为它是「正则写错一个字符类导致误判」的经典案例：

> The body alternation is `[^{}]*` (**not** `[^}]*`) so the regex matches only innermost `selector { body }` rules. With `[^}]*`, an outer `@media (...) { .display { font-size: 48px; text-transform: uppercase; … } }` matches as a single rule whose selector is the `@media (...)` wrapper and whose body begins with `.display { font-size: …` — so `parseDeclarations()` sees the first property as `.display { font-size`, not `font-size`, **the same-rule font-size is lost, and `hasAdequateUppercaseTracking()` falls back to the lenient inherited-size path that accepts 1px tracking on a 48px heading.**

一个字符类的差别，导致 `@media` 里的大写标题逃过检查。

**③ 三层「假阳性」防护**

- **剥 HTML 注释**（`lint-artifact.ts:124-127`）：注释里常有教学示例（「paste a `<section class="slide">` here」），不剥会误报
- **剥 CSS 注释**（`lint-artifact.ts:322-327`）：`/* .eyebrow { text-transform: uppercase; } */` 浏览器不渲染，但规则形状的正则会匹配
- **`declarationLaundersIndigo` / `isTokenShapedDeclaration`**（`lint-artifact.ts:935-958`）：区分「把 indigo 定义成 token」和「直接把 indigo 当 accent 用」

**④ 阈值都有理由**（`lint-artifact.ts:415-419`）：

> Allow up to ~12 raw hex values outside `:root`. **Device chrome (mobile-app frame: bezel gradient, side rails, status icons) has legitimate hardware-specific values in the 8–10 range**; raise the threshold so seed templates pass without ceremony.

12 这个数不是拍脑袋的，是「手机边框种子模板合法用到 8–10 个」倒推出来的。

### 13.3 反馈回路：`<artifact-lint>` 系统提醒

`renderFindingsForAgent()`（`lint-artifact.ts:519-537`）把发现渲染成一段贴回 agent 的系统提醒：

```
<artifact-lint>
The artifact you just produced has the following anti-slop / design-token issues.
2 P0 (must fix), 1 P1 (should fix), 0 P2 (nice to have).
Re-emit a corrected `<artifact>` in your next turn — do not write a separate explanation;
the user has the previous version already.

**[P0] ai-default-indigo** — Found #6366f1 used as a solid accent …
  Fix: Use var(--accent) from the active design system …
  Snippet: `background: #6366f1`
</artifact-lint>
```

三个细节：**按严重度排序**（P0 优先）、**明确要求重发修正版而不是写解释**、**每条都带 `fix` 而不只是 `message`**——给 agent 的报错必须自带修复动作，否则它会去猜。

### 13.4 一个诚实的边界

`craft/README.md:68`：

> The linter reports these as findings back to the UI (for P0/P1 badges) and to the agent (as a system reminder for self-correction). **Artifact persistence is not currently hard-blocked on P0 hits.**

也就是说：P0 命中**不会阻止落盘**。这是一个合理的取舍——硬阻断会让「模型死循环修不好」变成「用户什么都拿不到」。

> 🧠 **一句话**：`lint-artifact.ts` 是这十个项目里独一份的东西。别的项目做「工具输出正确性」的校验，OD 做的是**审美质量的程序化校验**。它把 `craft/anti-ai-slop.md` 那份人类可读的规则手册，和一份 1 000 行的执法代码，用注释里反复出现的「keep this list in sync with…」绑成了一对。

---

<h2 id="ch14">第 14 章 质量闸门二：Design Jury 五陪审员评审剧场</h2>

产品名 **Design Jury**，内部代号 **Critique Theater**（`docs/critique-theater.md:8-13`：代码路径 `apps/daemon/src/critique/`、web 侧 `components/Theater/`、SSE 频道 `critique.*`、环境变量 `OD_CRITIQUE_*`；**用户可见名来自单个 i18n key `critiqueTheater.userFacingName`，所以改产品名不用动代码**）。

### 14.1 五位陪审员与加权合成

`docs/critique-theater.md:41-47` + `packages/contracts/src/critique.ts:17-18`：

```ts
export const PANELIST_ROLES = ['designer', 'critic', 'brand', 'a11y', 'copy'] as const;
```

| 角色 | 评什么 | v1 权重 |
|---|---|---|
| **Designer** | 版式、构图、层次——「这东西好看且平衡吗」 | **0.0** |
| **Critic** | 是否真的满足简报；对比度、字重、可读性 | **0.4** |
| **Brand** | token 合规、语气、品牌色使用 | **0.2** |
| **Accessibility** | WCAG、焦点环、语义结构、alt 文本 | **0.2** |
| **Copy** | 语气、简洁度、错误文案质量 | **0.2** |

`critique.ts:58-73` 的默认配置：

```ts
weights: { designer: 0, critic: 0.4, brand: 0.2, a11y: 0.2, copy: 0.2 },
maxRounds: 3, scoreScale: 10, scoreThreshold: 8.0,
perRoundTimeoutMs: 90_000, totalTimeoutMs: 240_000,
parserMaxBlockBytes: 262_144, fallbackPolicy: 'ship_best',
maxConcurrentRuns: 4, enabled: false,
```

**为什么 Designer 权重是 0？**（`docs/critique-theater.md:65-68`）

> Designer is weighted at zero in v1 because **their dimensions are aesthetic preferences rather than ship gates**. The slot exists so the Designer's qualitative notes still travel into the transcript, and a future config release can bump the weight without changing the schema.

保留席位、权重归零——让定性意见进入记录，但不让主观审美卡住发布。这是一个很成熟的产品决策。

**配置校验带 epsilon 容差**（`critique.ts:48-54`）：

```ts
.refine((cfg) => cfg.scoreThreshold <= cfg.scoreScale + 1e-9,
        { message: 'scoreThreshold must be <= scoreScale' })
```

注释解释：「Small epsilon tolerance so a fractional threshold that rounds up against an integer scale (e.g. 8.0 with floating-point slack) still validates.」——浮点比较的教科书处理。

### 14.2 一个 CLI 会话、一条传输

`docs/critique-theater.md:76-82` 是这个设计最关键的一点：

> The orchestrator **does not spawn extra processes per panelist**. Each panelist is a **turn in the same agent session**, separated by `<PANELIST role="...">` tags in the protocol stream. The parser converts those into `panelist_*` events … This keeps the operational contract identical to a normal generation: **same auth, same env, same logs.**

以及（`docs/critique-theater.md:32-35`）：

> All five panelists are turns in the same conversation, which keeps the model context coherent and **prevents the "panelist disagrees with itself across processes" failure mode.**

对比一下：很多多 agent 评审系统会开五个进程/五个会话，结果是五份互相不知道对方的上下文，评出来的东西自相矛盾，而且鉴权、环境变量、日志全部要复制五份。OD 的做法把这些问题一次性消掉。

### 14.3 收敛循环

```mermaid
flowchart TB
    ART["Agent 发出 &lt;artifact&gt;"] --> R1["Round 1：五位陪审员各自打分<br/>（同一会话的五个回合）"]
    R1 --> COMP["composite = designer×0 + critic×0.4<br/>+ brand×0.2 + a11y×0.2 + copy×0.2"]
    COMP --> GATE{"composite ≥ 8.0 ?"}
    GATE -->|是| SHIP["Shipped at round N, composite X.X"]
    GATE -->|否| CNT{"round < maxRounds(3) ?"}
    CNT -->|是| REV["发出轮次摘要 → Agent 修改 → 下一轮"]
    REV --> R1
    CNT -->|否| FB["fallbackPolicy"]
    FB --> SB["ship_best（默认）：取 composite 最高的那轮"]
    FB --> SL["ship_last"]
    FB --> FL["fail"]
```

结算后折叠成一个徽章，五种状态（`docs/critique-theater.md:151-160`）：`Shipped` / `Below threshold` / `Timed out` / `Interrupted` / `Degraded`。

**`Interrupted` 的文案是单独写的**（`docs/critique-theater.md:159-161`）：

> The `interrupted` chip uses a distinct copy ("Interrupted at round N, best composite X.X") **so the user is not told the run shipped when it did not.**

### 14.4 四级开关解析器

`docs/critique-theater.md:88-110` 定义了优先级从高到低的四层：

| 优先级 | 层 | 说明 |
|---|---|---|
| 1（最高） | **技能级 `od.critique.policy`** | `required` 强制开、`opt-out` 强制关、`opt-in` 只在 M2+ 开 |
| 2 | **项目级覆盖** | web 的 setter 写 `localStorage`（会话内 UI）+ 对 `PATCH /api/projects/:id` 做**读-合并-写** |
| 3 | **`OD_CRITIQUE_ENABLED` 环境变量** | 高级用户 / CI fixture |
| 4（最低） | **灰度阶段默认** | M0/M1 = false，M2 = `opt-in` 技能为 true，M3 = 全开 |

第 2 层的读-合并-写有一处防数据丢失的细节（`docs/critique-theater.md:98-104`）：

> GET the current project, merge `critiqueTheaterEnabled` into the existing metadata blob, PATCH the merged object so other metadata fields (`kind`, `templateId`, `linkedDirs`, etc.) survive. **If the prefetch GET fails the setter skips the PATCH entirely instead of stomping the row.**

**技能作者的经验法则**（`docs/critique-theater.md:134-136`）：产出确定性产物的技能（如 `od-export-pdf`）通常 `opt-out`；生成全新设计输出的（`magazine-poster`、`saas-landing`）设 `required`。

### 14.5 降级与适配器一致性

五种降级原因（`critique.ts:76-83` + `docs/critique-theater.md:185-191`）：

| 原因 | 起因 | 处置 |
|---|---|---|
| `malformed_block` | 适配器发的 `<CRITIQUE>` 块解析器不认 | 跑一致性测试 `tests/critique-conformance.test.ts` |
| `oversize_block` | 超过 `parserMaxBlockBytes`（256 KB） | 通常是模型跑飞，重试或加预算 |
| `adapter_unsupported` | 该适配器被标 `critique:degraded`，**24h TTL** | 等 TTL 过期，或调 `clearDegraded(adapterId)` |
| `protocol_version_mismatch` | 适配器协议版本旧 | 升级适配器或钉住协议协商 |
| `missing_artifact` | run 结束但没有 `<artifact>` 主体 | 「Almost always a prompt bug; check the skill template.」 |

**一致性门槛**（`docs/critique-theater.md:211-216`）：

> The conformance harness runs every adapter prerelease against **10 brief templates**. If an adapter drops under the **90% shipped** or **95% clean-parse** thresholds for **two consecutive cycles**, it gets marked `critique:degraded` for 24h. The mark auto-clears on the next clean cycle.

**M3 全量开放的条件**（`docs/critique-theater.md:118-121`）：

> Enabled by default per skill during M2, then globally during M3 **after ≥ 90% of production adapters maintain conformance for 14 consecutive days.**

一个跨 25 条第三方 CLI 的功能，用「连续 14 天 ≥90% 一致性」作为全量开关——这是很硬的工程纪律。

### 14.6 可回放的评审

`docs/critique-theater.md:162-177`：每次 run 写一份结构化 `.ndjson`（可选 `.gz`）记录。Replay 按钮挂一个只读 Theater 按选定速度回放：`Instant`（同步冲刷全部事件）/ `Live`（按原节奏）/ `{ intervalMs: N }` / `Paused`（暂停后从光标处继续，**不重新冲刷已发事件**）。回放面可键盘拖拽：`J`/`K` 逐轮跳，`Esc` 退出。

---

<h2 id="ch15">第 15 章 插件与原子：<code>atoms</code> + <code>pipeline</code> + 封闭的 <code>until</code> 词汇表</h2>

### 15.1 插件的最小形状

`README` + `plugins/spec/SPEC.md`：一个插件必须有 `open-design.json`，加上**按类型决定的载荷**：

```
my-plugin/
├── open-design.json    ← 必需：市场元数据 + inputs + pipeline + capabilities
├── SKILL.md            ← agent-skill / scenario 类型必需，其他类型可省
├── README.md           ← 可选
├── preview/            ← 可选：index.html / poster.png（视觉类插件强烈建议）
└── examples/           ← 可选
```

核心字段：`specVersion`（当前 `1.0.0`）· `name`（稳定 ID）· `version`（semver）· `compat.agentSkills[].path` · `od.kind`（`skill`/`scenario`/`atom`/`bundle`）· `od.taskKind`（`new-generation`/`figma-migration`/`code-migration`/`tune-collab`）· `od.mode` · `od.capabilities[]` · `od.inputs[]`。

**能力最小声明原则**：「**declare the minimum** — a restricted install grants only `prompt:inject` by default」。

**官方插件的六个层级**（实测目录数）：

| 层级 | 数量 | 内容 |
|---|---|---|
| `scenarios/` | **13** | `od-default` · `od-design-refine` · `od-figma-migration` · `od-code-migration` · `od-react-export` · `od-nextjs-export` · `od-vue-export` · `od-media-generation` · `od-new-generation` · `od-tune-collab` · `od-plugin-authoring` · `od-share-to-community` · `od-web-effect-extractor` |
| `image-templates/` | **45** | 一次性图像提示 |
| `video-templates/` | **64** | HyperFrames / Seedance / Veo 动效模板 |
| `design-systems/` | **143** | 品牌 `DESIGN.md` 包成插件 |
| `atoms/` | **13** | 可复用 UI 片段 + 原子能力体 |
| `examples/` | **183** | 可重混的参考产出 |

### 15.2 原子目录：daemon 暴露给插件的能力单元

`docs/atoms.md:1-8`：

> A **plugin** assembles atoms into ordered stages (`od.pipeline.stages[].atoms[]`). The Open Design daemon is responsible for resolving each atom into a **system-prompt fragment, tool gating, and (when applicable) GenUI surface declarations**. **Plugins never own the atom implementations; they only reference them by id.**

已实现的原子（`docs/atoms.md:26-48`，真理源 `apps/daemon/src/plugins/atoms.ts`）：

| id | 干什么 | 适用 task kind |
|---|---|---|
| `discovery-question-form` | 第 1 回合问卷 | new-generation, tune-collab |
| `direction-picker` | 定稿前 3–5 个方向选择 | new-generation, tune-collab |
| `todo-write` | TodoWrite 驱动的计划 | 全部 |
| `file-read` / `file-write` / `file-edit` | 项目 cwd 的文件操作 | 全部 |
| `research-search` | Tavily 支撑的浅层调研 | new-generation |
| `media-image` / `media-video` / `media-audio` | 通过配置的 provider 生成媒体 | new-generation, tune-collab |
| `live-artifact` | 创建/刷新 live artifact | new-generation, tune-collab |
| `connector` | Composio 连接器工具调用 | new-generation, tune-collab |
| **`critique-theater`** | 五维面板评审，**发出驱动收敛的 `critique.score` 信号** | 全部 |
| `code-import` | 克隆/读取已有仓库 | code-migration |
| `design-extract` | 从源码 / Figma / 截图提取设计 token | code-migration, figma-migration |
| `figma-extract` | 提取 Figma 节点树 + token + 资源 | figma-migration |
| `token-map` | 把提取的 token 映射到活跃设计系统 | code-migration, figma-migration |
| `rewrite-plan` | 长跑多文件重写计划 | code-migration, tune-collab |
| `patch-edit` | 小步文件补丁 | code-migration, tune-collab |
| `build-test` | 跑 build/typecheck/tests，产出收敛信号 | code-migration |
| `diff-review` | 把重写渲染成可评审 diff | code-migration, tune-collab |
| `handoff` | 把产物推给下游（cli / cloud / desktop） | tune-collab |

### 15.3 流水线执行：五步 + 一行审计

`docs/atoms.md:50-66`：

```mermaid
flowchart TB
    M["插件 manifest<br/>od.pipeline.stages[*].atoms[]"] -->|① 解析| PS["PipelineStage[]<br/>plugins/pipeline.ts"]
    PS -->|② run 开始前| BODIES["解析内置原子指令体<br/>plugins/atom-bodies.ts<br/>← plugins/_official/atoms/&lt;atom&gt;/SKILL.md"]
    BODIES --> BLOCK["渲染成 ## Active stage 提示词块"]
    BLOCK --> RUN["③ 运行时 pipeline-runner.ts 走每个阶段"]
    RUN --> E1["发 pipeline_stage_started SSE"]
    E1 --> W["④ 向 atoms/registry.ts 的 worker 要<br/>daemon 可观测的信号"]
    W --> AUDIT["⑤ 往 run_devloop_iterations 写一行审计"]
    AUDIT --> E2["发 pipeline_stage_completed + 信号"]
    E2 --> RUN
```

一处诚实的说明（`docs/atoms.md:68-72`）：

> Atoms whose work happens inside the selected agent CLI may use the registry's **permissive compatibility signals** because **the daemon has no independent observation for that tool action.** This is distinct from the old global stub; `OD_PIPELINE_RUNNER=stub` exists only as a diagnostic/replay escape hatch.

也就是说：`file-write` 这类实际发生在 CLI 内部的原子，daemon 观察不到真实结果，只能给宽松信号。文档不假装它能观测一切。

### 15.4 封闭的 `until` 词汇表

`docs/atoms.md:74-87` 定义了六个可用于阶段收敛判定的信号：

| 信号 | 由谁发出 |
|---|---|
| `critique.score` | `critique-theater` 原子 |
| `iterations` | 内建的每阶段计数器 |
| `user.confirmed` | `confirmation` GenUI 面解析时 |
| `preview.ok` | live-artifact 预览流水线 |
| `build.passing` | build-test 的 build/typecheck 闸门 |
| `tests.passing` | build-test 的测试闸门 |

**关键的一句**（`docs/atoms.md:86-87`）：

> The evaluator is **deliberately closed and is not arbitrary JavaScript.** Unknown signals fail parsing and `od plugin doctor` reports them.

这是一个非常重要的安全 + 可维护性决策。市场上的插件如果能写任意 JS 作为收敛条件，你就得沙箱它、审计它、担心它无限循环。封闭词汇表把「插件能表达什么」限制在 daemon 能保证的语义内，代价是表达力受限——但换来的是**插件市场可以开放安装**。

### 15.5 原子的晋升路径

`docs/atoms.md:89-100` 定义了一条从「外部插件」到「一等原子」的路：

1. 先按 spec §22.5 把原子**作为插件在树外实现**
2. `SKILL.md` / MCP 工具 / pipeline 形状稳定后，在 `plugins/_official/atoms/<id>/` 加内置原子，往 `FIRST_PARTY_ATOMS` 追加一行，**并在 daemon 有真实可观测信号时注册一个 worker**
3. 同一个 PR 里更新本文档和 spec 的 §10 / §21 / §23 表格
4. 之后可通过 `od.pipeline.stages[*].atoms[]`、`GET /api/atoms`、`od atoms list/show/info`、`od plugin doctor` 触达

---

<h2 id="ch16">第 16 章 安全边界：loopback / SSRF / 桌面 HMAC / <code>.od-skills</code> 拷贝</h2>

`docs/architecture.md:245-261` 列了七条边界。逐条拆开看实现：

### 16.1 默认 loopback，公开部署要显式三件套

> The daemon binds to loopback by default. Non-loopback/public deployment requires **explicit bind, origin, and authentication configuration.**

README 补充：LAN 暴露需要显式 `OD_BIND_HOST` **加** `OD_ALLOWED_ORIGINS`。**连接器凭证和 live-artifact 预览路由无论如何都保持 loopback-only。**

### 16.2 SSRF：默认封锁内网，opt-out 是严格精确匹配

README 里那段是全仓库对 SSRF 讲得最细的：

> To prevent SSRF, the daemon blocks provider base URLs that resolve to private/internal address ranges (**RFC1918, link-local, CGNAT, and cloud-metadata IPs**) by default, surfacing `Internal IPs blocked`.

内网自托管网关（LiteLLM、Ollama 在 VPN 的 `10.x`）通过 `OD_ALLOWED_INTERNAL_HOSTS` 放行，规则极其严格：

| 性质 | 说明 |
|---|---|
| **严格 opt-in** | 默认空 |
| **精确主机匹配** | **不做**子域名/子串匹配 |
| **格式宽容** | 接受 `host:port` 或完整 URL，归约到 hostname；IPv6 必须加方括号 `[fd00::1]` |
| **范围受限** | **只**作用于你自己配置的 provider 端点（连接测试、模型发现、BYOK 聊天） |
| **不放宽下游** | **刻意不**放宽上游响应里返回的下载 URL——那些仍然被封 |
| **错误项丢弃** | 畸形条目、或 **CIDR 记法（不支持）** 会被丢弃并告警，而不是静默信任 |
| **信任传递已知** | 「Allowlisting a hostname trusts whatever it resolves to; allowlist the resolved IP instead if you want the DNS-resolved address re-checked.」 |

最后一条尤其重要：它**明说了**放行主机名意味着信任 DNS 解析结果（DNS rebinding 风险），并给出了更严格的替代方案。这种「说清楚残余风险」的文档，比「我们很安全」有价值得多。

### 16.3 桌面信任：短寿命单次 HMAC 令牌

`docs/architecture.md:228-243`：

```mermaid
sequenceDiagram
    participant U as 用户
    participant M as Electron main（可信）
    participant R as Renderer（沙箱）
    participant D as Daemon

    U->>M: 点「导入文件夹」
    M->>U: 原生文件夹选择器
    U->>M: 选中 /Users/me/work/site
    M->>M: 铸造短寿命、单次 HMAC 令牌
    M->>R: 令牌 + 路径
    R->>D: POST /api/import/folder（带令牌）
    D->>D: 校验 HMAC + 规范化路径<br/>拒绝落在自己托管存储内的导入
    D->>D: 打上「服务端控制的可信选择器标记」
    D-->>R: 项目创建成功
    Note over D: 之后每次文件访问都对该外部根做安全路径解析
```

关键的一句：**「Imported projects that may use the desktop open-path bridge carry a server-controlled trusted-picker marker; ordinary project create/update requests cannot forge it.」**——这个标记由服务端控制，普通的项目创建/更新请求**伪造不了**。实现在 `apps/daemon/src/desktop-auth.ts` 和 `import-export-routes.ts`。

### 16.4 沙箱 iframe：按需开启最小特性

`docs/architecture.md:250-253`：

> Artifact and plugin previews run in **sandboxed iframes without host same-origin access**; individual surfaces **opt into only the sandbox features they require**, such as downloads or popups.

预览的 URL vs srcDoc 决策由 `apps/web/src/components/file-viewer-render-mode.ts` 单独负责（`docs/architecture.md:164-169`）。要用检查、评论选中、调色/编辑/微调这些**宿主集成**能力，必须走 srcDoc 路径。切换渲染模式时**两个 frame 都保持挂载**以避免重载闪烁，消息处理器**校验发送方 iframe**，需要来自活跃 frame 的信号会**再次核对活跃窗口**。

### 16.5 spawn 权限：诚实地承认这是「受信任的 agent 执行」

`docs/agent-adapters.md:442-465` 这一节的坦率程度值得表扬。它先说明为什么：

> The daemon delegates policy enforcement to each CLI, but its headless arg builders **intentionally choose non-interactive permission modes.** The effective project cwd is **an execution root, not a uniform Open Design sandbox**, and external-directory flags can widen a CLI's reach.

然后**逐条列出每个 CLI 被喂了什么危险 flag**：

| CLI | headless 权限姿态 |
|---|---|
| Claude | `--permission-mode bypassPermissions` |
| Cursor | `--force` + 能力门控的 `--trust` |
| Devin | `--permission-mode dangerous --respect-workspace-trust false` |
| Qoder / Trae | `--yolo` |
| Copilot | `--allow-all-tools` |
| DeepSeek | `--auto` |
| Amp | `--dangerously-allow-all` |
| OpenCode | `--dangerously-skip-permissions`，**且仅当 help 探测确认该 flag 存在时**（老版本不给未知选项） |
| Codex | macOS/Linux 默认 `workspace-write` + 网络；Windows/WSL 或显式 `OD_CODEX_SANDBOX=danger-full-access` 用 `danger-full-access`（因为 workspace-write 路径在那里撑不住必需的 shell 执行） |

结论说得毫不含糊：

> users must treat these runs as **trusted agent execution with the authority shown by the selected definition.**

**为什么必须这样？** `docs/agent-adapters.md:314` 给了理由：「the daemon runs agent CLIs **without a TTY**, so it must not rely on an interactive tool-approval prompt to make progress.」没有 TTY 就没法交互批准，交互批准会直接把 run 挂死。

> ⚠️ **这是 OD 最大的一处安全取舍**，也是它区别于本系列所有其他项目的地方：openworker 有五档权限模式和收件箱挂起、CodeWhale 有 safe-by-construction、nanobot 有 bwrap 沙箱——OD **主动放弃了权限闸门**，把它整个让渡给被托管的 CLI，然后用文档把这件事说清楚。理由是自洽的（无 TTY + 委托策略），但用户必须知道自己在授权什么。

### 16.6 Windows 命令行长度：三重守卫

`docs/agent-adapters.md:345` 那段（DeepSeek 适配器）是全文档最长的一个 bullet，讲的是一个非常具体的跨平台地狱。DeepSeek 的 clap 声明 `prompt: String` 是必填位置参数，没有 `-` stdin 哨兵，所以提示词只能进 argv。于是 `/api/chat` 上了**三道互补守卫**：

| 守卫 | 时机 | 检查什么 |
|---|---|---|
| `checkPromptArgvBudget` | **bin 解析前**（快速） | 原始组装提示词的字节数 vs `maxPromptArgBytes`（DeepSeek 声明 30 000） |
| `checkWindowsCmdShimCommandLineBudget` | `buildArgs` 之后 | 解析出的是 Windows `.cmd`/`.bat` shim 时，用**平台层相同的逐参数引号翻倍规则**重算 `cmd.exe /d /s /c "<inner>"` 的完整命令行 |
| `checkWindowsDirectExeCommandLineBudget` | `buildArgs` 之后 | 解析出的是非 shim 的 Windows 安装（如 cargo 构建的 `deepseek.exe`）时，用 **libuv 的 `quote_cmd_arg` 规则**（每个 `"` 变 `\"`，紧邻引号的反斜杠翻倍）重算 |

两个 Windows 守卫在给定解析上**互斥**：cmd-shim 守卫管 `.cmd`/`.bat`，direct-exe 守卫管其余。三者一起抓的是「**原始字节数没超，但引号密集的提示词（代码块、JSON 形状的技能种子）展开后超过 CreateProcess 的 32 767 字符 `lpCommandLine` 上限**」这种情况。

三者发同一个可行动的 `AGENT_PROMPT_TOO_LARGE` SSE 错误，告诉用户「减少技能/设计系统上下文、缩短对话、或换一个支持 stdin 的适配器」。并且三者都有单测（超长 + 短提示词分支、两条 Windows 路径的引号密集回归、互斥性检查），「so the guards can't silently regress」。

> 🧠 **一句话**：这一段是「支持一个不肯读 stdin 的 CLI 要付多少代价」的完整账单。它也解释了为什么 `claude.ts` 那句注释要把 `MAX_ARG_STRLEN ~128 KB` 和 `CreateProcess ~32 KB / .cmd shim ~8 KB` 三个数字都写出来——这是踩过的坑。

---

<h2 id="ch17">第 17 章 功能特性拾遗</h2>

### 17.1 `od` CLI 与 MCP 服务端：把 OD 变成别人的工具

OD 出货三种形态：**skills、CLI、MCP server**。一行安装（README）：

```bash
od mcp install <agent>
# <agent> = claude | codex | reasonix | raven | cursor | copilot | openclaw
#         | antigravity | pi | vibe | hermes | cline | kimi | kiro | trae | opencode
```

装完之后在任意 agent 里：

```bash
od project list --json
od files list <project-id> --json
od files read <project-id> <relative-path>
od plugin list --json
od skills list --json
```

**为什么用 MCP 而不是导出 zip？** README 的理由很实在：「Exporting and re-attaching a zip every iteration breaks flow. MCP exposes the design source directly — **the agent always sees the live file, not a stale export.**」

**安全模型**：默认只读，daemon 绑 `127.0.0.1`，SSRF 在代理边缘拦截。

**一个真实的命名冲突**：macOS / WSL2 上 `/usr/bin/od` 是系统的八进制转储工具，会在 PATH 上盖过 Open Design 的 `od`。README 三处提醒用户改用**设置 → MCP server** 里那段用绝对路径的片段。`install.sh` 的存在理由也很务实：「it exists so the hosted URL returns shell instead of the landing-page HTML fallback and **fails fast if your shell resolves a non-Open-Design `od` binary**」。

### 17.2 BYOK 代理：没装 CLI 也能跑

`POST /api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream`——粘 `baseUrl` + `apiKey` + `model` 就有同样的循环，不 spawn 进程。预置了 OpenAI、Atlas Cloud、Anthropic、Azure OpenAI、Google Gemini、Ollama、LM Studio、vLLM 及任意 OpenAI 兼容端点。

但注意 `docs/agent-adapters.md:206-218` 讲的 **BYOK OpenCode**：

> The former direct-Anthropic fallback was **replaced** by the `byok-opencode` profile. API-mode provider credentials and model selection are translated into OpenCode configuration, while **the installed `opencode-cli`/`opencode` process still owns the model/tool loop.**
> **There is no daemon-owned fallback loop and no daemon implementation of `Read`/`Write`/`Edit` tools**, and this profile is **not** an automatic recovery target for failed local agents.

早期版本大概真的在 daemon 里实现过一个「直连 Anthropic + 自己实现 Read/Write/Edit」的兜底循环。现在被彻底删掉了——因为那违背了「我们不实现 Agent 循环」这条根本主张。**这是一次值得尊敬的架构回归。**

### 17.3 没有跨 agent 兜底链

`docs/agent-adapters.md:411-416`：

> Open Design does **not** implement an ordered cross-agent fallback chain. A chat request explicitly names its agent, and a crash, auth failure, timeout, or invalid invocation **remains a failure for that run.** The user can select another agent and send the request again, but **the daemon does not silently — or through a dedicated one-click fallback action — move the request to Claude, another detected CLI, or BYOK OpenCode.**

只有两个窄得多的规则：
- **运行前默认**（`agentId` 省略时）：用配置的 agent 如果它当前可用，否则用第一个可用定义。「This chooses an agent **before** a run starts; it is **not** failure recovery.」
- **过期会话恢复**：存的原生会话过期时，daemon 可以清掉**同一个 agent** 的陈旧会话并用完整 transcript 重新播种。「That recovery **never changes agent families**.」

这是一个反直觉但正确的产品决策：**自动切换 agent 会让计费、鉴权、输出风格全部悄悄改变**，用户根本不知道刚才那份产物是谁做的。

### 17.4 Live Artifact vs Normal Artifact

`CONTEXT.md:11-25` 的术语区分很重要：

| 概念 | 定义 | 避免说成 |
|---|---|---|
| **Normal Artifact** | 由一个**产物入口文件** + 一份**产物清单**表示的项目设计产出 | live artifact、通用文件上传 |
| **Live Artifact** | 可刷新的项目设计产出，存成 live-artifact 记录，带源数据和预览状态 | normal artifact、静态产物 |
| **Artifact Entry File** | 打开/渲染一个 normal artifact 的主项目文件 | 支持文件、资源、旁挂文件 |
| **Artifact Manifest** | 把项目文件标记为 normal artifact 的旁挂元数据，记录 kind / renderer / exports / entry | live-artifact 文档、项目元数据 |
| **Active Project** | 用户在 OD UI 里最近交互的项目，MCP 工具在未指定项目时可用它 | latest project、default project |

`CONTEXT.md:100-107` 甚至给了一段**示例对话**和一条**已解决的歧义记录**：

> **Dev:** "When a coding agent creates a Codex deck through MCP, should it create a live artifact?"
> **Domain expert:** "No. Unless the user asked for refreshable data, create a **Normal Artifact**."
>
> Flagged ambiguities: "artifact creation" was used to mean both Normal Artifact creation and Live Artifact creation; **resolved: this capability creates Normal Artifacts only.**

### 17.5 HyperFrames：HTML → MP4

HeyGen 开源的 agent 原生视频框架，作为一等公民集成。agent 写 **HTML + CSS + GSAP**，HyperFrames 用 **headless Chrome + FFmpeg** 渲染成确定性 MP4。仓库带 **11 个 HyperFrames 模板 + 39 个 Seedance 提示**。OD 特有的渲染工作流（合成缓存、sandbox-exec 变通、MP4-as-chip）在 `design-templates/hyperframes/`。

### 17.6 导出矩阵

| 格式 | 实现方式 |
|---|---|
| **HTML** | 单文件、内联所有资源 |
| **PDF** | 浏览器打印，deck 感知 |
| **PPTX** | agent 驱动的技能（`pptx-html-fidelity-audit` 技能有 10 个文件专门做保真审计） |
| **ZIP** | 归档 |
| **Markdown** | — |
| **MP4** | HyperFrames |

### 17.7 mock agent：不烧额度的回归测试

`mocks/mock-agent.mjs` + `mocks/golden/` + `mocks/recordings/`。`apps/daemon/AGENTS.md:112`：

> For agent-stream/parser changes, **replay a mock CLI trace from `mocks/` when practical instead of burning provider budget.**

这是维护 26 条适配器的必需品——每次改解析器都真跑 25 个 CLI 是不可能的。

---

<h2 id="ch18">第 18 章 总结：三个最独特的设计与三处取舍</h2>

### 18.1 三个最独特的设计

#### ① 适配器是数据规格，不是类——把「支持 N 个 CLI」的成本从 O(N) 压到 O(1)

`RuntimeAgentDef` 里 **45 个字段，只有 `buildArgs` 一个函数，而且是纯的**。探测、启动、调用、取消、解析全在共享引擎里。结果：

- 新增一条已知 wire format 的 CLI = **一个对象字面量 + registry 里加一行**，最小的定义只有 **629 字节**（`defs/kilo.ts`）。
- 九条 ACP 适配器共用一个传输层。
- 引擎里**零个 per-agent 分支**——`docs/agent-adapters.md:514` 那句「The engine is agent-agnostic: it iterates `AGENT_DEFS` and reads fields」是可以对着代码验证的。

**它的克制同样关键**：契约里**没有** `nativeSkillLoading`、**没有** `skillInjectionStrategy`、**没有** `capabilities()` 方法、**没有** `surgicalEdit` 特性门表。所有想往定义里塞行为的诱惑都被文档明确拒绝了。

#### ② 把「审美质量」变成程序化闸门——`lint-artifact` + Design Jury 双层

这是本系列十个项目里**独一份**的能力。别的项目做「工具调用是否正确」的校验，OD 做的是「这东西看起来像不像 AI 拉的」的校验：

- **第一层 `lint-artifact.ts`（1 000 行）**：16 条规则，带 CSS 变量递归求值（深度 4）、主题作用域解析、字号折算的字距判定、三层假阳性防护。规则表和 `craft/anti-ai-slop.md` 用注释里的「keep in sync」绑成一对。
- **第二层 Design Jury**：五位陪审员在**同一个 CLI 会话**里以五个回合评分，加权合成，≥8.0/10 才发货，最多 3 轮，超时/不达标走 `ship_best`。跨 25 条第三方 CLI 用「连续 14 天 ≥90% 一致性」作为全量开关。

而且**两层都不硬阻断**：P0 命中不阻止落盘、评审不达标走 ship_best。这是「有闸门但不锁死用户」的成熟取舍。

#### ③ 提示词的缓存分区排序——把 hermes 的「前缀神圣」推进了一步

`composeSystemPrompt` 把 20+ 个片段按**变化频率**分成四带（全局静态 → 会话稳定 → 项目稳定 → 回合可变），并且：

- 触发信号的**稳定性决定块的位置**：项目创建时固定的元数据信号可以插在项目稳定带；对话中途可能翻转的文本信号必须推到回合可变后缀，因为「an early insert would break the cached prefix for every section after this line」。
- 配套 `describeStablePromptCache()` 做**命中归因**，未命中时逐段 diff 出是哪一段漂移了，并且刻意不在「无基线」的情况下报告全量变化以免淹没信号。
- slim 变体还把方向库改成「索引 + `od tools directions --id` 按需拉」——但**只在 filesystem 执行画像下**，因为 text_artifact 档没有工具去解引用索引。

### 18.2 三处必须知道的取舍

#### 取舍一：主动放弃权限闸门

OD 给每个 CLI 喂的都是最危险的非交互 flag：`bypassPermissions` / `dangerous` / `--yolo` / `--allow-all-tools` / `--dangerously-allow-all` / `--auto`。理由自洽（无 TTY，且策略应由 CLI 自己执行），文档也坦率（「treat these runs as trusted agent execution」），但事实是：

**在本系列里，openworker 有五档模式 + 收件箱挂起，nanobot 有 bwrap 沙箱，CodeWhale 有 safe-by-construction，Claude Code 有权限闸门——OD 一个都没有。** 它的安全预算全花在了**边界**上（loopback、SSRF、HMAC、沙箱 iframe、`.od-skills` 拷贝），而不是**执行**上。

对个人本地使用，这是合理的；对团队共享部署，这需要非常认真的评估。

#### 取舍二：两份提示词组装器必须逐字节同步

daemon 侧 `prompts/system.ts`（2 075 行）和 contracts 侧 `prompts/system.ts`（1 064 行）是**两份独立实现的同一套语义**。仓库里至少六处注释在提醒「keep both in sync」「keep this whole block BYTE-IDENTICAL」。

这是「同一套设计流程既要服务本地 CLI 执行、又要服务 BYOK 直连」付出的税。理论上可以抽公共层，但 daemon 侧要读文件系统、要拼 craft、要接 critique，contracts 侧必须保持纯净（`apps/daemon/AGENTS.md:96`：「Keep `packages/contracts` pure. Do not move daemon-only Node, SQLite, Express, filesystem, or process types into contracts.」）。这个漂移风险是**已知且被显式管理**的，但它是真实的技术债。

#### 取舍三：内容库的体量本身就是维护负担

151 个设计系统包 × 每包至少 3 个文件、164 个技能、115 个渲染模板、277 个官方插件、183 个示例、107 个媒体提示模板——加起来 2 246 个 Markdown、2 493 个 HTML。仓库 322 MB。

这些内容的**质量守卫**已经写了很多（`pnpm guard`、`pnpm lint:craft`、包质量守卫、派生文件一致性校验、token 契约、组件 fixture、来源证据、预览覆盖），但内容腐烂的速度和内容增长的速度是同一个量级的。三个月 82k star 的增长曲线意味着社区贡献会持续涌入——**这套守卫能不能扛住，是这个项目未来一年最大的不确定性。**

### 18.3 它在这十个项目里的位置

| 维度 | 其余九个编程/任务 Agent | **Open Design** |
|---|---|---|
| **主循环** | 各自实现 | **不实现**，委托给 25 个 CLI |
| **工具系统** | 自己定义 40–74 个工具 | **零个**，用被托管 CLI 的原生工具 |
| **权限模型** | 从 safe-by-construction 到五档模式 | **无**，全部委托 + 边界防御 |
| **上下文工程** | 压缩、裁剪、双轨记忆 | **分层组装 + 缓存分区**（20+ 层） |
| **质量保证** | 测试通过 / 编译通过 | **审美 linter + 五陪审评审** |
| **扩展机制** | 插件 / MCP / 技能 | **四平面**：skills + templates + design-systems + craft |
| **交付物** | 代码变更 | **HTML/PDF/PPTX/MP4 真实文件** |
| **产品形态** | CLI / TUI / IDE 插件 | **本地优先桌面 App + MCP 服务端 + Docker** |

> 🧠 **最后一句**：如果说 openworker 回答的是「怎么让模型像同事一样干完跨应用的活」，那 Open Design 回答的是另一个问题——**「已经有一堆很强的编程 Agent 了，怎么让它们一起变成一个设计师？」** 它给出的答案不是再造一个 Agent，而是造一整套**它们都能读懂的文件系统**（技能 + 模板 + 品牌 + 工艺），加上一层**它们都能被同一套数据规格描述的插座**，再加上两道**它们都必须通过的审美闸门**。
>
> 这个答案对不对，三个月 82k star 已经给了一个初步的市场判断。但它真正的工程价值在于：**它证明了「适配器即数据」这条路能撑住 25 个异构 CLI，而且证明了「设计质量」这件看起来纯主观的事，有相当大一部分是可以被程序化执法的。**

---

## 附录 A · 源码导览索引

| 想看什么 | 去哪个文件 |
|---|---|
| 适配器契约（45 个字段） | `apps/daemon/src/runtimes/types.ts:101-253` |
| 26 条定义的注册与去重不变式 | `apps/daemon/src/runtimes/registry.ts:30-76` |
| 一条完整定义的样本 | `apps/daemon/src/runtimes/defs/claude.ts`（98 行） |
| 探测流水线 | `apps/daemon/src/runtimes/detection.ts:238-318` |
| 启动路径解析（探测与执行必须一致） | `apps/daemon/src/runtimes/launch.ts` |
| 裸文本流的 `<artifact>` 提取 | `apps/daemon/src/runtimes/plain-stream.ts`（473 行） |
| Claude stream-json 解析 | `apps/daemon/src/runtimes/claude-stream.ts`（25.9 KB） |
| ACP 传输（九条适配器共用） | `apps/daemon/src/agent-protocol/acp/session.ts`（40.7 KB） |
| 执行画像映射（249 字节） | `packages/contracts/src/execution-profile.ts` |
| 提示词组装（daemon 侧） | `apps/daemon/src/prompts/system.ts:791-1370` |
| 提示词组装（BYOK 侧镜像） | `packages/contracts/src/prompts/system.ts:256-496` |
| 三条硬规则 + 设计哲学 | `packages/contracts/src/prompts/discovery.ts:25-354` |
| 设计师宪章（改编自 claude.ai/design） | `packages/contracts/src/prompts/official-system.ts:11-163` |
| 缓存命中归因 | `apps/daemon/src/runtimes/chat-prompt-inputs.ts:394-442` |
| 反 AI 味 linter | `apps/daemon/src/lint-artifact.ts`（1 000 行） |
| 评审剧场配置与 schema | `packages/contracts/src/critique.ts` |
| 评审编排器 | `apps/daemon/src/critique/orchestrator.ts`（37.8 KB） |
| 技能暂存（安全修复现场） | `apps/daemon/src/cwd-aliases.ts:1-60` |
| 技能注册表与 mode 归一 | `apps/daemon/src/skills.ts`（42 KB） |
| 原子目录与流水线 | `apps/daemon/src/plugins/atoms.ts` · `pipeline.ts` · `pipeline-runner.ts` |
| 桌面信任 HMAC | `apps/daemon/src/desktop-auth.ts` |
| 预览渲染模式决策 | `apps/web/src/components/file-viewer-render-mode.ts` |

## 附录 B · 关键文档索引（这个仓库的文档质量值得单独学）

| 文档 | 为什么值得读 |
|---|---|
| `docs/agent-adapters.md`（37 KB） | 全仓库最重要的一份。含契约、探测策略、12 条适配器逐条注记、授权边界、开放问题 |
| `docs/architecture.md`（13 KB） | 含一段罕见的「历史否定说明」——列出被实现推翻的早期草案 |
| `docs/skills-protocol.md`（17 KB） | 技能格式、发现优先级、七种 mode、craft 订阅、暂存规则 |
| `docs/critique-theater.md`（10.7 KB） | 五陪审员、四级开关、降级原因表、回放、一致性门槛 |
| `docs/atoms.md`（5.6 KB） | 原子目录、流水线解析、封闭 `until` 词汇表、晋升路径 |
| `docs/modes.md`（5.9 KB） | 六个创建标签 × 七种 skill mode 的刻意错位 |
| `CONTEXT.md`（6.4 KB） | 领域语言词典，每个术语带 `_Avoid_`（不许用的同义词） |
| `AGENTS.md`（37.9 KB）+ 各层 `AGENTS.md` | 仓库所有权规则、数据根契约、dual-track 规则 |
| `craft/README.md`（6.3 KB） | 第四轴的立论、两级执法、运行时宽容 vs 仓库严格 |
| `design-systems/README.md`（5.8 KB） | 包契约、派生文件即缓存、七个 H2 下限 |

---

*本文基于 `nexu-io/open-design` commit `c893b60`（2026-07-28）逐文件核对写成。所有 `文件:行号` 引用均可回源码验证；统计数字（文件数、目录数、字节数）由 GitHub Git Tree API 全量遍历计算。*
