# Open Design source analysis: an agent product that does not write an agent—turning “design” into a filesystem any CLI can consume

> **Subject**: open-design (`nexu-io/open-design`), against **commit `c893b60`** (2026-07-28 01:48 UTC, `main`). License **Apache-2.0**.
> **Code scale**: repo **11 745 files / 321.9 MB**. Of which TypeScript `.ts` **2 341 (56.7 MB)**, `.tsx` **455 (9.7 MB)**, HTML **2 493 (22.1 MB)**, Markdown **2 246 (26.9 MB)**. Three core blocks: `apps/daemon/src/` 465 TS files (6.9 MB), `apps/web/src/` 472 (16.5 MB), `packages/` 235 (1.4 MB).
> **Community scale**: **82.0k stars / 9.5k forks / 701 open issues**, repo created 2026-04-28—**three months**.
> **One-sentence position**: **the open-source stand-in for Claude Design**. It itself **implements no agent loop**; it treats the **26 runtime defs (25 distinct local CLI executables)** already on your machine—`claude` / `codex` / `cursor-agent` / `copilot` / `opencode` and the rest—as “design engines,” feeds them a filesystem of **skills + design systems + craft rules + plugins**, then takes the **real files** they produce (HTML / PDF / PPTX / MP4) back into preview and export.
> **Audience**: a reader who has already read at least one analysis in this series. This lecture keeps the “mark `file:line`, walk back to source” style, but **chapters expand from 12 to 18**—because Open Design is not the tenth coding agent; it is **the host of those agents**, and the layers that need analysis are completely different.

---

> 🗺️ **Companion architecture diagram**: this project has a dedicated figure in the [**seven-agent architecture gallery**](../架构图库.html#ch3)—**multi-process trust topology + extension-point figure (adding the 26th CLI changes 1 data file)**.
> Every gallery figure first states “what question it answers” and “the load-bearing claim,” then goes through three review and render-acceptance rounds.

## Contents

**Part I · What it is**
1. [Project overview: an agent product that does not write an agent loop](#ch1)
2. [Panorama: four processes · four content classes · one SSE](#ch2)
3. [Three run shapes and the single truth of the “data root”](#ch3)

**Part II · The adapter layer (the most core layer)**
4. [Adapter contract: `RuntimeAgentDef` is a data spec, not a class](#ch4)
5. [Probe and capability negotiation: why you must probe “the path that will actually be spawned later”](#ch5)
6. [Four stream formats: from `claude-stream-json` to ACP to naked text](#ch6)
7. [Two execution profiles: `filesystem` vs `text_artifact`](#ch7)

**Part III · Context engineering (the real product moat)**
8. [Input dispatch: six create surfaces × seven skill modes](#ch8)
9. [The heart: twenty layers of `composeSystemPrompt` and cache bands](#ch9)
10. [Three hard rules: question-form → brand branch → TodoWrite](#ch10)
11. [Memory and the double loop: task-brief / verify-scorecard / rule-proposal](#ch11)

**Part IV · Content planes and quality gates**
12. [Four content planes: skills / design-templates / design-systems / craft](#ch12)
13. [Quality gate 1: `lint-artifact` programmatic anti-AI-taste](#ch13)
14. [Quality gate 2: Design Jury five-juror review theater](#ch14)
15. [Plugins and atoms: `atoms` + `pipeline` + a closed `until` vocabulary](#ch15)

**Part V · Boundary and wrap-up**
16. [Security boundary: loopback / SSRF / desktop HMAC / `.od-skills` copy](#ch16)
17. [Feature leftovers: MCP server, export, HyperFrames, Live Artifact, automation](#ch17)
18. [Wrap-up: three most distinctive designs and three trade-offs](#ch18)

---

<h2 id="ch1">Chapter 1 · Project overview: an agent product that does not write an agent loop</h2>

### 1.1 The concrete problem it wants to solve

In April 2026 Anthropic shipped **Claude Design**—the first time a large model no longer spat prose, but directly delivered **finished design artifacts**. It took off, but it is closed-source, paid, pure-cloud, locked to Anthropic’s models / skills / UI: you cannot self-host, cannot swap the agent, cannot deploy on Vercel.

Open Design (hereafter **OD**) is its open-source stand-in. The README’s position sentence is precise:

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

Split that sentence into three verifiable engineering claims:

| Claim | What it looks like in code |
|---|---|
| **Agent-agnostic** | Under `apps/daemon/src/runtimes/defs/`, **26 object literals**, each describing how to call one CLI; the engine has zero per-agent branches |
| **Brand can be contracted** | Under `design-systems/`, **151 packages**, each `manifest.json` + `DESIGN.md` + `tokens.css`, stuffed whole into the system prompt |
| **Deliver real files** | The agent writes files in the project cwd; `FileViewer` renders them in a sandboxed iframe; export HTML/PDF/PPTX/ZIP/MP4 |

### 1.2 The fundamental difference from the first nine projects in this series

In the previous nine analyses (opencode / hermes-agent / Raven / CodeWhale / goose / nanobot / grok-build / OpenManus / Suna / openworker), **every one implements its own main loop**: call the model, parse tool use, execute, assemble context, call the model again. They compete on “whose loop is better.”

OD **does not have a single line of main loop**. `docs/agent-adapters.md:5` says this very bluntly:

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

And the thesis (`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.**

> 🧠 **One sentence**: other projects are building engines; OD is building **the socket for engines**, then dumping all its energy on “after the engine is running, how to make what it produces look like a human made it”—that is, the three things prompt engineering + content library + quality gates.

### 1.3 Digitized project shape

```
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 侧插件
```

**Several magnitudes worth remembering first**:
- **26 runtime defs** (`registry.ts:30-57`), corresponding to **25 distinct local CLI executables**—`byok-opencode` reuses OpenCode’s executable, so the def count is one more than the binary count.
- **Prompt code volume is larger than the engine**: `apps/daemon/src/prompts/system.ts` **2 075 lines / 125 KB**; the mirrored `packages/contracts/src/prompts/system.ts` **1 064 lines / 68 KB**. By contrast `runtimes/registry.ts` is only **81 lines**.
- **`apps/web/src/components/FileViewer.tsx` single file 660 KB**—the largest source file in the whole repo; preview/inspect/comment/export all live in it.

---

<h2 id="ch2">Chapter 2 · Panorama: four processes · four content classes · one SSE</h2>

### 2.1 Process topology

`docs/architecture.md:57-73` gives a plain-text figure; translated into mermaid with the omitted details filled in:

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

Four boundaries that must be remembered:

1. **The web UI and the `od` CLI call the same daemon HTTP API.** `docs/architecture.md:75-77` is explicit: “The CLI is not a second business-logic implementation; it is the machine-readable surface for the same capabilities.” This is the **dual-track rule** the repo keeps stressing (`apps/daemon/AGENTS.md:99-106`): adding a user-perceivable capability must in the same change complete contract types + daemon route + web UI + `od` subcommand.
2. **The protocol is HTTP + SSE, not WebSocket.** The architecture doc opens with a dedicated “Historical note” (`docs/architecture.md:11-16`) saying the earliest draft drew Vercel-tunnel mode, browser-direct mode, WebSocket `session.generate`, in-memory session bus, `history.jsonl`, three watch-style skill registries—**all overturned by the implementation**. This kind of “negative documentation” is rare in open source, and a good signal of whether a spec is trustworthy.
3. **The daemon is the only privileged process.** It binds loopback, owns SQLite, credentials, project files, spawn rights. The web is only a shell.
4. **The child process is not OD’s code.** This is the easiest point to mix up: when you see “thinking / calling a tool / writing a file” in OD, that is Claude Code or Codex running their own loop; OD is only translating their stdout into unified events and drawing them.

### 2.2 Four content planes

OD splits “make the model produce good design” into four mutually orthogonal axes (`craft/README.md:9-16`’s table is the clearest official statement):

| Axis | Directory | API | What it governs | Examples |
|---|---|---|---|---|
| **Functional skills** | `skills/` (164) | `GET /api/skills` | **Capability** the agent calls while working | `brand-extract` · `web-clone` · `design-brief` |
| **Rendering templates** | `design-templates/` (115) | `GET /api/design-templates` | Renderable **finished shapes** | `saas-landing` · `dashboard` · `html-ppt-*` |
| **Design systems** | `design-systems/` (151) | `GET /api/design-systems` | **Brand contract**: palettes, type, spacing, components | `apple` · `stripe` · `linear-app` |
| **Craft rules** | `craft/` (11 `.md`) | Assembled by the daemon on demand | **Brand-independent universal rules** | ALL CAPS must have ≥0.06em tracking |

The fourth axis `craft/` is OD’s smartest split relative to Claude Design. `craft/README.md:18-21` explains it well:

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

Skills **subscribe on demand** to craft rules via frontmatter (`docs/skills-protocol.md:245-251`):

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

**Only the listed paragraphs enter the prompt**—a skill that only does type does not pay token cost for color or motion content. 11 published slugs: `typography` · `typography-hierarchy` · `typography-hierarchy-editorial` · `color` · `anti-ai-slop` · `state-coverage` · `animation-discipline` · `accessibility-baseline` · `rtl-and-bidi` · `form-validation` · `laws-of-ux`.

> 🧠 **One sentence**: splitting “how the brand looks” (`DESIGN.md`, which changes) from “a professional designer’s muscle memory” (`craft`, which does not) is the key to 151 brand packages not copying the same typesetting common sense from each other.

### 2.3 The registry’s “user-root first” scan

All four content classes follow the same discovery rule (`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["返回列表"]
```

Three deliberate designs:
- **Rescan on every request**—no watcher, no `SIGHUP`, no daemon restart. Drop a folder into the directory, refresh the page, it appears.
- **Shadow, not overwrite**: a same-name user entry **shadows** the built-in entry; the built-in file is not touched. Delete the user edition, the built-in edition comes back automatically.
- **Chat-time resolution crosses two registries**: because in a persisted project `skillId` may be either a functional skill or a design template (`docs/architecture.md:141-143`). Compatibility bought with historical baggage.

---

<h2 id="ch3">Chapter 3 · Three run shapes and the single truth of the “data root”</h2>

### 3.1 Three shapes

| Shape | Entry | Who starts the daemon | Who starts web | Desktop shell |
|---|---|---|---|---|
| **Source development** | `pnpm tools-dev run web` | tools-dev sidecar | tools-dev sidecar | not started |
| **Packaged desktop / packaged headless** | `apps/packaged` | packaged launcher | packaged launcher | Electron (headless edition skips) |
| **Container / daemon serves directly** | `docker compose up -d` | single process | **the same daemon directly serves `apps/web/out` static export** | none |

`docs/architecture.md:22-31` has a very practical engineering constraint:

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

That is: ports are temporary; **identity is not decided by the port**. In packaged-desktop mode, Electron even **does not assume a port**—it asks the web’s real URL via sidecar IPC (`docs/architecture.md:36-39`). This contrasts with OpenWorker’s fixed 8765 port + token file: OD demotes the port completely to a transport detail.

### 3.2 The “data-root contract”: a deliberate documentation discipline

This is the most interesting **meta-rule** in the whole repo. `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.

The README repeats the same ban (“This README MUST NOT restate it”). `apps/daemon/AGENTS.md:97` adds another cut: “Route all daemon-owned data through `RUNTIME_DATA_DIR` or constants derived from it.”

Why lift this out on its own? Because **several documents each hard-coding a path** is the classic rot source of every local-first app: the implementation changes, six of eight md files still write the old path, the user following the docs cannot find the data. OD’s practice is to demote the path to a **single-point authority**; every other document may only cite, not restate.

At boot `apps/daemon/src/server.ts` resolves `OD_DATA_DIR` once into `RUNTIME_DATA_DIR`; afterwards SQLite, hosted project workspaces, artifacts, user registry entries, credentials, automation state, plugin state **are all derived from this root** (`docs/architecture.md:150-155`).

**The only exception is folder import**: imported projects use the **external** `metadata.baseDir` the user picked; the daemon validates and boundary-limits that workspace, rather than copying it into the hosted root (`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">Chapter 4 · Adapter contract: <code>RuntimeAgentDef</code> is a data spec, not a class</h2>

This chapter is the **layer most worth learning** in the whole project.

### 4.1 The counter-intuitive design decision

Facing the need “support 25 CLIs,” 99% of teams would write an abstract base class:

```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 did not. `docs/agent-adapters.md:17` original:

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

The contract body is in `apps/daemon/src/runtimes/types.ts:101-253`, **~45 fields**, of which only **6 required**:

```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 个可选字段，全部是数据或纯函数
};
```

**Key property**: `buildArgs` is the only function field, and it is **pure**—input prompt and options, output argv array. It does not spawn, does not read files, does not own lifecycle. So:

- **Probe** is done uniformly by `detection.ts`;
- **Launch** is done uniformly by `launch.ts`;
- **Invoke** is done uniformly by `invocation.ts`;
- **Cancel** is done uniformly by the generic run lifecycle;
- **Parse** is dispatched by `streamFormat` to one of 4 parsers.

`docs/agent-adapters.md:27` can therefore make this promise:

> **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 The registry: 81 lines + one boot-time invariant

`apps/daemon/src/runtimes/registry.ts` is only 81 lines in full; the structure is worth looking at as a whole:

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

That `throw` executes at **module-load time**—not a runtime check, **load equals fail**. A user-custom profile colliding with a built-in id, the daemon does not start, rather than quietly overwriting the built-in adapter. This is textbook “let the error surface as early and as loudly as possible.”

`readLocalAgentProfileDefs` (`local-profiles.ts`) lets the user define **local profiles** in config—for example the same `claude` binary with a different base URL, a different default model, registered as two different adapters. They **inherit the base adapter’s `authProbe`**, but via `authProbe.classifierAgentId` (`types.ts:244`) explicitly carry “whose auth classifier to use,” so they do not fall back to the generic classifier.

### 4.3 One real definition: `claude.ts` line by line

`apps/daemon/src/runtimes/defs/claude.ts` is 98 lines in full, the best sample for understanding the whole contract.

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

**① Fallback binaries (`claude.ts:20-24`)**. The comment states the motive:

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

One array solves “the ecosystem grew an argv-compatible fork”; the user does not need to write a wrapper script.

**② Capability probe (`claude.ts:30-39`)**:

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

The comment hides an autopsy of a real 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` is a “substring → capability key” map: after probing `--help` output, a hit substring sets `agentCapabilities[id][key]` to `true`. `buildArgs` then reads it to decide whether to add the flag.

**③ Model discovery (`claude.ts:40-44`)**:

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

Comment: `claude` has no list-models subcommand, so prefer reading local mmd/MMS routes (so Claude-compatible models behind a proxy also appear in the picker), then fall back to built-in aliases.

**④ The real argv build (`claude.ts:52-89`)**—the core of the whole def:

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

Notice the underscore on `_prompt`—**the prompt does not enter argv at all**. Why? The comment at `claude.ts:45-51` is the most practical stretch of cross-platform knowledge in the whole repo:

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

**⑤ Session resume (`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.
```

This design is very important; Chapter 6 will expand it: OD **does not** each turn re-assemble the whole conversation history for the CLI; it **lets the CLI keep its own session**.

**⑥ The remaining three declaration fields (`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 Three “session resume” styles

`types.ts:189-209` uses three boolean fields to distinguish three sharply different resume mechanisms, a fineness of classification I have not seen in other projects:

| Field | Style | Who generates the session id | Representatives |
|---|---|---|---|
| `resumesSessionViaCli` | **specify-style** | **the daemon generates** `newSessionId`, tells the CLI to use it | `claude --session-id <uuid>` |
| `capturesSessionIdFromStream` | **capture-style** | **the CLI generates it itself**, reports it from the stream | `codex`’s `thread.started.thread_id` |
| `resumesSessionViaAcpLoad` | **ACP-style** | take `getDurableSessionId()` from the ACP session | AMR / Vela, resume with `session/load` |

The comment (`types.ts:191-199`) explains why they must be split: specify-style’s id is the one the daemon stored; capture-style must be grabbed from the parsed stream (surfaced as a `status` event’s `sessionId`); `newSessionId` **is not passed to the CLI at all**. Confusing the two leads to “thinking you are resuming, actually opening new every time.”

There is an even subtler **side effect** of the `resumesSessionViaCli` field (`types.ts:70-84` long comment, about `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.**

This is an extremely concrete bug autopsy: **double context** makes the model re-pop the discovery form on turn 2, looking like “the loop is stuck.” The fix is not changing the prompt, but adding an opt-out mark so the daemon on this class of adapter **skips the rendered web transcript** and only sends the latest user message.

### 4.5 The contract’s boundary: what it deliberately does not contain

`docs/agent-adapters.md:176-181` has a stretch of “negative specification,” no less valuable than the positive:

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

Skill delivery is **shared daemon behavior** (Chapter 12 details it), not a per-adapter strategy. Likewise `docs/agent-adapters.md:376-378`:

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

`capabilities.ts` is only **131 bytes** in full—it is only an in-memory map of “flags found in `--help` output,” not a capability model.

> 🧠 **One sentence**: this contract can hold 26 adapters because it is extremely restrained and only describes **“how to talk to this CLI,”** leaving **“what to say”** (the prompt) and **“how to schedule”** (the engine) entirely in the shared layer. Every temptation to stuff behavior into the def is explicitly refused.

---

<h2 id="ch5">Chapter 5 · Probe and capability negotiation: why you must probe “the path that will actually be spawned later”</h2>

### 5.1 Probe pipeline

`apps/daemon/src/runtimes/detection.ts:238-318`’s `probe()` is each adapter’s probe body:

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

**① Why must `resolveAgentLaunch` come first?** This is the line most worth remembering in the whole chapter; the comment at `detection.ts:243-250` is a complete field record:

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

In human language: a GUI-launched App’s PATH is stripped by the system; an nvm-installed shim cannot run in it; but the launch resolver can upgrade to the packaged native binary. If probe uses path A and the real run uses path B, the user sees “not installed” but it can actually run—**probe and execute must walk the same path resolution**. This is a hard constraint.

**② Version probe’s three-state verdict** (`docs/agent-adapters.md:121-124`):

| Result | Verdict |
|---|---|
| OS-level missing / not executable | **unavailable** |
| Can start, but refuses the `--version` flag | **available**, version left empty |
| Success | available + version |

Tolerance of the second case is key: some CLIs have renamed the version flag; that should not kill them.

**③ The motive for concurrency is written into the comment** (`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.**

**④ Per-adapter fault isolation** (`docs/agent-adapters.md:130`): “Each adapter probe is fault-isolated so one broken executable cannot empty the whole picker.” The comment near `detection.ts:361` specially mentions: without this protection, a naked `Promise.all` would reject the whole because one rejected.

### 5.2 Auth: only assert on adapters that declared a probe

This is a **deliberate subtraction**. `docs/agent-adapters.md:126-128`:

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

The old version’s practice was “look whether `~/.foo/` exists” to guess login—the cost of guessing wrong is marking a usable agent with a red cross. The current rule: **no declared probe means unknown**; real auth failure is inferred only from **error text of a real run failure** (`classifyAgentServiceFailure`).

The comment at `types.ts:228-234` states this evolution clearly:

> 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’s probe is `['auth', 'status']`, 5-second timeout (`claude.ts:26-29`).

### 5.3 Probe UX: streaming return

`docs/agent-adapters.md:427-431`: `GET /api/agents?stream=1` emits one `agent` SSE event each time a probe finishes, finally `done`. The settings panel therefore **does not wait for the slowest CLI** before it can start drawing cards. The non-stream version returns the full array.

**No 24-hour probe cache** (`docs/agent-adapters.md:112-114`)—every call re-probes concurrently. This is a trade-off of “rather spend a few more seconds than show the user stale state.”

---

<h2 id="ch6">Chapter 6 · Four stream formats: from <code>claude-stream-json</code> to ACP to naked text</h2>

### 6.1 Seven `streamFormat`s, four classes of parser

The grouping table at `docs/agent-adapters.md:139-147` (aligned with `registry.ts`’s 26 defs):

| `streamFormat` | Runtime ids | Parser |
|---|---|---|
| `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, further dispatched by `eventParser`) |
| `copilot-stream-json` | `copilot` | `src/copilot-stream.ts` (the only parser laid flat in `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` |

Nine ACP adapters share **the same transport**—this is the direct reason “adding an ACP agent needs only one def file.” The sentence at `docs/agent-adapters.md:27` “A genuinely new wire format is the only case that also adds an engine file” is verified here: after the ACP ecosystem grew, the marginal cost of adding an agent dropped to an object literal of a few hundred bytes (`defs/kiro.ts` 637 bytes, `defs/vibe.ts` 635 bytes, `defs/kilo.ts` 629 bytes).

### 6.2 `<artifact>` extraction from a `plain` stream: a parser worth reading line by line

`runtimes/plain-stream.ts` is 473 lines in full. The problem it solves: **a naked-text-stream CLI has no structured file-write tool call; how do you land its output as project files?**

The convention is Anthropic-style source blocks (`docs/agent-adapters.md:357-362`):

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

The mapping table is at `plain-stream.ts:53-64`:

| `type` | File it lands as |
|---|---|
| `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` |

**But the real brilliance is the four pits it avoids**:

**Pit 1 — fake `<artifact>` inside Markdown fences (`plain-stream.ts:78`, `252-306`)**. The model often, when explaining “you should write it like this,” puts `<artifact>` inside a ```` ``` ```` fence or backticks. A naïve `indexOf('<artifact')` would write the teaching example to disk as a real artifact. So before parse, compute **skip ranges**:

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

Fence open/close judged line by line with `FENCE_OPEN_RE = /^```(\w[\w+-]*)?\s*$/` and `FENCE_CLOSE_RE = /^```\s*$/` (`plain-stream.ts:50-51`); inline backticks must **match the same number of consecutive backticks** (`countBacktickRun` / `findMatchingBacktickRun`, `plain-stream.ts:349-375`). The comment also marks that this implementation **mirrors** `apps/web/src/artifacts/markdown-context.ts`—headless land-to-disk and browser parse must see the same fence boundaries.

**Pit 2 — prefix false positive like `<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' 后面必须是空白
}
```

**Pit 3 — `>` inside an attribute value (`plain-stream.ts:235-250`)**. `findOpenTagEnd` is a quote-aware state machine, not `indexOf('>')`: `title="A > B"` will not be truncated.

**Pit 4 — nested / unclosed (`plain-stream.ts:85-103`)**. Each time an open tag is found, first probe the position of the **next** open tag: if the next open tag appears before the current open tag ends, or appears before the close tag, the current one is bad; skip to the next and restart. So one stretch of malformed output will not swallow every later legitimate artifact.

Additionally a bomb-defense `MAX_ARTIFACTS_PER_RUN = 50` (`plain-stream.ts:44`) and filename dedupe (`reserveUniqueArtifactFileName`, `plain-stream.ts:398-419`: collisions add `-2`, `-3`, cap 10 000).

**On land, also write a manifest** (`plain-stream.ts:421-472`): each artifact generates an `artifactManifest` by extension, declaring `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'] }
```

This manifest is the **Artifact Manifest** defined at `CONTEXT.md:23-25`—“mark a project file as a formal artifact, and record its kind, renderer, export methods, entry file” sidecar metadata.

> 🧠 **One sentence**: `plain-stream.ts` is one of the highest “engineering-detail density” files in the whole repo. It proves one thing—**the weakest-capability adapter needs the most code written**.

### 6.3 Nested agents: an honest open question

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

That is: when Claude Code opens a child agent with the Task tool, the child’s `turn_end` is recognized but **not allowed to end the parent run**—otherwise the whole turn would close early. Content can display, but there is no nested tree in the UI. The docs explicitly list this under “Open questions,” rather than pretending it is solved.

---

<h2 id="ch7">Chapter 7 · Two execution profiles: <code>filesystem</code> vs <code>text_artifact</code></h2>

This is OD’s core abstraction for “adapter capability is uneven,” a file of only 249 bytes:

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

`streamFormat === 'plain'` (and BYOK/API execution) → `text_artifact`; the rest → `filesystem`.

### 7.1 Two delivery paths

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

**The two profiles’ prompt contracts are opposite**, and both are written dead into the prompt:

- `filesystem` profile: `FILESYSTEM_HANDOFF_OVERRIDE` (`apps/daemon/src/prompts/system.ts:549-572`) explicitly says “**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”, and “A filesystem run that emits a source-code `<artifact>` is treated as an **unexpected fallback** by the host.”
- `text_artifact` profile: `API_MODE_OVERRIDE` (`contracts/src/prompts/system.ts:513-517`) says the reverse: “**No tools are wired through to you.** `TodoWrite`, `Read`, `Write`, `Edit`, `Bash`, `WebFetch` are unavailable”, and “The override does **NOT** block `<artifact>` blocks — those are how the web UI receives finished HTML in API mode.”

### 7.2 Why `API_MODE_OVERRIDE` must be pinned at the very top

The comment at `contracts/src/prompts/system.ts:296-307` is a complete bug report:

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

Here is a classic prompt-engineering game: **the discovery layer itself opens by writing “the following rules override everything later.”** To beat it, the only clean method is to pin the API-mode override at the **absolute top**. The JSDoc at `contracts/src/prompts/system.ts:498-512` records this reason in full—the most serious engineering comment on “prompt priority” I have seen.

The same logic is copied as-is on the daemon side (`daemon/src/prompts/system.ts:920-932`); the comment explicitly requires “Keep the wording **byte-identical** to the contracts copy so both code paths produce the same observable behaviour.”

> ⚠️ **This is also a cost of this architecture**: the daemon and contracts two prompt assemblers must stay **byte-sync**. The repo has at least 6 comments reminding this (`system.ts:369-375`, `386-388`, `920-927`, etc.). This is the tax paid for “the same semantics must serve both CLI execution and BYOK execution.”

---

<h2 id="ch8">Chapter 8 · Input dispatch: six create surfaces × seven skill modes</h2>

### 8.1 Two classification systems deliberately not one-to-one

`docs/modes.md:5-11` opens the door:

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

**Six UI-side create tabs** (truth in `CreateTab` / `MediaSurface` of `apps/web/src/components/NewProjectPanel.tsx`):

| Tab | Project metadata | Skill routing | Main difference |
|---|---|---|---|
| **Prototype** | `kind: prototype` | Default `prototype` skill, replaced by the selected design template | Responsive web / mobile / tablet / desktop app UI |
| **Live Artifact** | `kind: prototype`, `intent: live-artifact` | A `prototype` skill with live-artifact capability | High-fidelity, with data / connectors |
| **Deck** | `kind: deck` | Default `deck` skill, replaced by the selected design template | Slide navigation + presentation export |
| **Template** | `kind: template` | A project template the user saved via Share | Starts from a template the user themselves stored, **does not** fall back to the built-in directory |
| **Media** | `kind: image` / `video` / `audio` | Media skill matching the mode | provider/model/aspect/duration/voice/prompt template |
| **Other** | `kind: other` | Does not require a skill | Freeform |

**Seven daemon-side `od.mode`s**: `prototype` · `deck` · `template` · `design-system` · `image` · `video` · `audio`.

Two misalignments that must be remembered:
1. **`design-system` is a legal skill mode, but is no longer a create tab** (`docs/modes.md:85-92`)—design systems were promoted to an independent product page.
2. **Live Artifact is only prototype + intent** (`docs/modes.md:43-48`)—“Code that branches on project kind alone must therefore also check the intent.” This is a hidden pit that will be stepped in.

### 8.2 “Template replaces the default skill,” not “template stacked on the default skill”

`docs/modes.md:36-41` tells a rule easy to misread:

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

That is: pick the `saas-landing` template, and there **is no** default prototype skill. List separation (two APIs) **does not equal** automatic composition. This is deliberate—injecting both would produce fighting workflow instructions.

### 8.3 HyperFrames: a new entry that does not add a backend kind

`CONTEXT.md:39-41` records a very smart product/engineering compromise:

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

The frontend gives it an independent entry (user-perceived first-class citizen); the backend reuses `video` kind + one `videoModel` value (does not touch the `ProjectKind` union, does not touch the SQLite schema, does not touch existing projects). Every term in `CONTEXT.md` carries a line **`_Avoid_`**—listing **synonyms you must not use**. This “domain-language dictionary” itself is an engineering practice worth copying.

---

<h2 id="ch9">Chapter 9 · The heart: twenty layers of <code>composeSystemPrompt</code> and cache bands</h2>

If other agent projects’ heart is the main loop, OD’s heart is this function.

- **Daemon edition**: `apps/daemon/src/prompts/system.ts:791-1370` (~580-line function body, file 2 075 lines)
- **Contracts edition (for BYOK/API)**: `packages/contracts/src/prompts/system.ts:256-496`

### 9.1 Input: `ComposeInput`’s 30+ fields

The destructure list at `daemon/src/prompts/system.ts:791-831` is this layer’s entire input surface:

```
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 Assembly order (classic variant)

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

Note that **⑭ and ⑳ say the same thing twice**: design-system content is injected at layer 14; layer 20 then patches an override “**do not ask the user for a visual direction again**.” Why repeat? Because layer 6’s discovery layer has a hard rule “turn 1 must send a questionnaire,” and the questionnaire usually has brand/direction questions. Layer 20’s `ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE` (`daemon/src/prompts/system.ts:603-616`) is specifically to press it:

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

**This is the real shape of prompt engineering**: not writing one pretty instruction, but **managing priority among thousands of lines of mutually conflicting rules**.

### 9.3 slim vs classic: a prompt refactor with A/B

`promptCoreVariant === 'slim'` walks a completely different assembly path (`daemon/src/prompts/system.ts:832-907`). The comment explains the difference:

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

This is a refactor still being validated: collapse “discovery layer + designer charter + tail override” these three blocks into one charter document. Both sets coexist, decided by A/B.

slim also carries two optimizations classic does not have:

**① The direction library becomes an index + pull-on-demand (`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.**

An optimization that only holds under an execution profile that “has tools”—OD carefully made a conditional branch.

**② Platform-contract block partitioned by signal stability (`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 Cache-friendly band ordering: the most exquisite engineering in the whole repo

The comment at `daemon/src/prompts/system.ts:851-867` is this chapter’s climax:

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

Split the prompt by **change frequency** into four bands:

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

**Why this order?** Because an LLM’s prefix cache is **prefix matching**: as long as the first N tokens are the same, it hits. Put the most stable first; a signal that flips mid-turn (the user says “change it to the iOS edition”) only invalidates the **cache suffix**, not the whole prompt.

There is also a matching `describeStablePromptCache()` (`runtimes/chat-prompt-inputs.ts:394-442`) for **cache-hit attribution**:

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

That last `changedSections` is computed only on **real drift**. The comment (`chat-prompt-inputs.ts:435-437`) explains: `missing-stored-hash` is an old row with no baseline; reporting “every section changed” there would drown the signal you actually care about.

> 🧠 **One sentence**: hermes-agent’s signature is “prefix cache is sacred”; OD takes the same idea one step further—**not only do not break the prefix, but tag every segment with a “change frequency” label, sort by frequency, and monitor which segment caused each miss**. This is the most mature prompt-cache engineering I have seen among these ten projects.

### 9.5 Ask mode: light, but not amnesiac

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

What is saved is **workflow**; what is kept is **context**. This distinction is key: when the user asks “why was this palette chosen this way,” they do not need the designer charter, but they need to know what the current design system is.

---

<h2 id="ch10">Chapter 10 · Three hard rules: question-form → brand branch → TodoWrite</h2>

`DISCOVERY_AND_PHILOSOPHY` (`packages/contracts/src/prompts/discovery.ts:25-354`, 330 lines of pure prompt) is the whole product’s **behavior script**. It is distilled from two upstream projects (`discovery.ts:18-21`): `alchaincyf/huashu-design` (junior-designer mode, variants rather than answers, anti-AI-taste, incarnate as an expert) and `op7418/guizang-ppt-skill` (preflight resource read, P0 self-check, theme rhythm).

### 10.1 Timing of the three rules

```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 How paranoid RULE 1 is

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

“The form is your time-to-first-byte”—this sentence punches through the product motive of the whole rule: **the user cannot tolerate 15 seconds of silence**, but can completely accept a form that pops in 2 seconds and can be clicked through.

And it explicitly blocks the excuse the model loves most (`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.

Only three cases allow skip (`discovery.ts:156-159`): a small tweak in an existing design, the user explicitly says “skip questions / just build,” the message starts with `[form answers — …]`.

### 10.3 Twelve engineering details in the form-authoring rules

The “Form authoring rules” stretch at `discovery.ts:135-152` has extremely high information density; pick the most representative:

**① Hard cap 5 questions (`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.**

**② Prefill is mandatory, and `default` must be written before `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.**

This is a **JSON key-order requirement born of streaming render**—a pure engineering detail leaking into the prompt, but completely reasonable.

**③ Branch values must stay English (`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.**

**Display layer fully localized, control layer fully English**—because RULE 2 must branch-match on `value`. And even “speak like a person, do not translate word-for-word” is written in.

**④ Do not write your own “Other” option (`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.**

**⑤ Metadata and plugin inputs are equivalent (`discovery.ts:149`)**: this one is the longest; the core is that the two sources “Project metadata” and “Plugin inputs” are **equally authoritative**; if either source provided an answer, delete the corresponding default question; only fields marked “(unknown — ask)” should add a new question. It even lists synonym-field mappings: `platform` / `surface` / `platformTargets` / `target` all answer “target platform”; `slideCount` / `slides` / `pageCount` all answer “page count.”

### 10.4 RULE 2’s branch-resolution order

`discovery.ts:167-172` defines a **four-step priority**:

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

**Branch A’s five-step brand extract** (`discovery.ts:176-187`), one tool call per step:

1. **Locate the source**: attachments, list them; URL, WebFetch `<brand>.com/brand`, `/press`, `/about`
2. **Download style artifacts**: CSS, brand-guide PDF, screenshots
3. **Extract ground truth**: `grep -E '#[0-9a-fA-F]{3,8}'` grab hexes in CSS; screenshots, look at typesetting with the eye. **“Never guess colors from memory.”**
4. **Encode into a contract**: write `brand-spec.md`, containing six OKLch color tokens (`--bg` `--surface` `--fg` `--muted` `--border` `--accent`), display/body/mono font stacks, 3–5 observed layout postures (radius, border weight, accent budget)
5. **Oral recap**: one sentence stating the system that will be used, so the user can cheaply correct

There is a hard anti-hallucination rule (`discovery.ts:178`): picked `brand_spec` / `reference_match` but **has not yet given a source**, then **ask for the source and stop**—“**Do not guess a brand domain or invent tokens.**”

**Branch B explicitly forbids a second direction ask** (`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.

This is precipitation of a product decision: early versions probably popped a “pick-one-of-five direction card”; later they found one more click is one more drop-off, changed to “pick yourself; if the user is unsatisfied, then talk.”

### 10.5 RULE 3’s nine-step plan template and two non-negotiable gates

The standard plan at `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.**”

**Five dimensions** (`discovery.ts:235-243`) are worth remembering as-is, because they are the human-readable edition of the later Design Jury:

| Dimension | The interrogation |
|---|---|
| **Philosophy** | Does the visual posture match the ask (editorial vs minimal vs brutalist)? Or did it drift back to your favorite default? |
| **Hierarchy** | Does each screen have one obvious eye landing? Or are all elements competing with each other? |
| **Execution** | Type, spacing, alignment, contrast—are they right, or only “about right”? |
| **Specificity** | Is every word, number, image exclusive to **this** brief? Or did filler and generic-data taste mix in? |
| **Restraint** | One accent used at most twice, one decisive highlight—or three highlights fighting? |

“Any dimension under 3/5 is a regression. Go back, fix the weakest, re-score. **Two passes is normal.**”—notice that last sentence: **the default should be two rounds of rework**; this takes the unrealistic expectation “one-shot finished” out of the flow.

### 10.6 Deck’s “framework first” iron law

`discovery.ts:223` lifts slides out on their own:

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

The assembler has the matching implementation (`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}`);
}
```

The comment on the three judgments (`system.ts:450-465`) explains each branch’s origin:
- The `metadata.kind === 'deck'` line is to catch **deck projects with no skill bound**—otherwise “neither a skill seed nor a framework skeleton, the agent writes scale/nav/print logic from zero, and steps in the same `place-items: center` + transform pit again.”
- **Skill seed first**: the seed itself defined a more opinionated framework (simple-deck’s scroll-snap, guizang-ppt’s magazine layout); pinning a generic skeleton would conflict.
- **Freeform projects** (the user skipped kind selection) walk a separate branch with a conditional prefix: “**If — and only if — the brief reads as slides, keynote, presentation, deck, PPT, or 讲解**, follow the framework below. Otherwise ignore everything in this section.”

These three branches are fossils of three production accidents.

---

<h2 id="ch11">Chapter 11 · Memory and the double loop: task-brief / verify-scorecard / rule-proposal</h2>

### 11.1 Memory is not a hard rule

The memory block’s preface (`contracts/src/prompts/system.ts:378`) defines a **conflict arbitration**:

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

The comment (`system.ts:369-375`) states this wording’s intent: “**what stops the model from treating remembered preferences as harder than the active design system**.” That is, prevent “the user last time said they like dark” from overriding “this time the brand is light.”

### 11.2 Memory as a “task-intent gateway”

The second half of `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 Three `<od-card>`s: turn internal state into interactive UI

OD defines a set of `<od-card type="…">` assistant text blocks; the host renders them as cards. Three types correspond to memory’s three loops:

| Card | When it is sent | Gated by | Role |
|---|---|---|---|
| `task-brief` | **PRE**: when a short request is expanded by memory into a full brief, emit one folded card at the very start of the reply | `memoryHooks.rewrite` (default ON) | Let the user see “what I understood it as,” can immediately correct |
| `verify-scorecard` | **POST**: after producing/editing an artifact, check “verified rules” one by one | `memoryHooks.verify` (default ON) | The daemon **programmatically checks** whether this card exists |
| `rule-proposal` | When the user’s correction implies a reusable, checkable rule | Always on | **Propose rather than silently save**; only takes effect after the user clicks Keep |

All three have strict behavior constraints:

**`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`)**—here **programmatic enforcement** appears:

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

And it specifies closing order:

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

And “Prefer fixing silently over asking. Leave a row as `fail` only when fixing it needs a decision you genuinely cannot make.”—**if you can fix it yourself, do not ask**.

**`rule-proposal` (`system.ts:402`)**—the most restrained:

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

The last half-sentence is treating a **concrete model-lying behavior**: the model loves to say “okay, I remembered,” but actually remembered nothing. OD’s solution is to **bind the assertion “remembered” to the existence of the card**.

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

> 🧠 **One sentence**: OD’s memory system differs from OpenWorker’s “explicit SQLite facts” and Raven’s “EverOS dual-track”—it is **with UI, auditable, and only lands in the store after the user clicks confirm**. Three cards turn “the model’s internal state” into “interface elements the user can see, change, refuse.” This is the most productized memory design I have seen among these ten projects.

---

<h2 id="ch12">Chapter 12 · Four content planes: skills / design-templates / design-systems / craft</h2>

### 12.1 Skill format: eat Claude Code’s `SKILL.md` as-is

`docs/skills-protocol.md:13-51`: the base format **does not change** Claude Code’s convention at all; OD only adds an optional `od:` namespace in frontmatter. The goal at `docs/skills-protocol.md:121` is written very clearly:

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

What happens if a skill writes no `od:` at all (`docs/skills-protocol.md:112-120`)?

| Field | Default inference |
|---|---|
| `mode` | Inferred from description and body, fallback `prototype` |
| `surface` | image/video/audio entries take the corresponding media, otherwise `web` |
| `preview.type` | `html` |
| `design_system.requires` | **`true`** (must explicitly write `false` to not get brand context) |
| `example_prompt` | First sentence of description, truncated to picker length |
| `scenario` | Inferred from description/body, fallback `general` |

### 12.2 `.od-skills` staging: a security fix from symlink to a real copy

This is the stretch of this chapter most worth learning. The file-header comment of `apps/daemon/src/cwd-aliases.ts:1-30` is a complete design note + security autopsy:

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

That is: **the agent has write permission on cwd**; if `.od-skills/` is a symlink pointing at the repo `skills/`, one `Edit` can corrupt the skill source file shared by all projects. This is a very real **write-amplification attack surface**.

Three matching details:

**① Cost argument (`cwd-aliases.ts:20-24`)**: only stage the **active** skill, not the whole `SKILLS_DIR`; a single skill is usually 1–3 MB; on APFS / btrfs / ReFS `fs.cp` walks CoW, steady-state cost is only a few syscalls.

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

**③ Alias carries a path hash (`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
}
```

Why the hash? Because the user root and the built-in root may have **same-name skills** (the shadow relation); when both are selected, directory names would collide. Hashing the source path solves it.

**④ Cross-filesystem fallback**: `fs.cp`’s underlying `copy_file_range(2)` is refused across filesystems (commonly `EXDEV`; copying a container image layer onto a ZFS/overlay bind mount will be `EPERM`). Node does not auto-degrade, so OD itself implemented a **recursive streaming-copy fallback** (`docs/agent-adapters.md:170-171` also mentions this).

**⑤ Two paths given in the prompt** (`docs/agent-adapters.md:174-176`): cwd-relative staged path (primary) + absolute source path (fallback), so **if staging fails the agent can still work**.

```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-system packages: three-file minimum contract + six-layer assembly order

**Package shape** (`design-systems/README.md:8-22`):

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

Constraints (`design-systems/README.md:44-49`): folder slug must equal `manifest.id` and use normalized ASCII; `files.design` is fixed as `DESIGN.md`, `files.tokens` fixed as `tokens.css`; **every declared path must be safe, relative, exist**.

**Rich files are cache, not source of truth** (`design-systems/README.md:79-86`):

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

**Injection order** (`docs/skills-protocol.md:200-213`)—this is how a design system actually enters the prompt:

```
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` specially clarifies three things **not** done:

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

The default usage contract (`daemon/src/prompts/system.ts:617`) is worth citing as-is:

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

The last half-sentence is preventing a concrete hallucination: the model sees the index and thinks the files have already been read.

**A floor of seven H2s, but titles not prescribed** (`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.**

This evolved from upstream `awesome-design-md`’s nine-section template: keep the “must be substantial enough” bar, drop the rigidity of “must be called these names.”

### 12.4 Craft’s two-level enforcement

`craft/README.md:63-70` defines an honest two-layer contract:

| Level | Meaning | Examples |
|---|---|---|
| **Auto-checked** | Wired into rules of `apps/daemon/src/lint-artifact.ts` | `anti-ai-slop.md`’s P0 list |
| **Guidance** | The rest: the agent reads, reviewers use, the linter does not check | `state-coverage.md`, `animation-discipline.md` |

And craft files **mark item by item** which are “(guidance, not auto-checked)” (see P1/P2 subsections of `craft/anti-ai-slop.md`). `craft/README.md:70`: “A purely behavioral craft file … is guidance **unless a specific rule is later promoted into `lint-artifact.ts`.`**”

**Runtime tolerant vs repo strict** (`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`.**

This is a very good “compatibility vs quality” dividing line: **runtime does not crash because one paragraph of docs is missing, but the repo does not let you write a wrong slug**. And “deliberate forward references” have an explicit registry, making “planned work” visible, rather than silently dropping a prompt paragraph via a typo.

---

<h2 id="ch13">Chapter 13 · Quality gate 1: <code>lint-artifact</code> programmatic anti-AI-taste</h2>

`apps/daemon/src/lint-artifact.ts`, **1 000 lines / 46 KB**. This is the most creative module in this repo: **drop a subjective judgment like “looks fake at a glance” into executable regex + CSS evaluation**.

### 13.1 Full rule table

`lintArtifact(rawHtml)` (`lint-artifact.ts:120-510`) returns `LintFinding[]`, each with `severity` / `id` / `message` / `fix` / `snippet`:

| Severity | id | What it checks | Lines |
|---|---|---|---|
| **P0** | `purple-gradient` | `linear-gradient()` contains one of 20 Tailwind violet/indigo hexes, or the literals `purple`/`violet` | `131-160` |
| **P0** | `trust-gradient` | Blue→cyan “trust gradient”: blue-family 13 hexes × cyan-family 8 hexes pairing | `177`, `559-635` |
| **P0** | `ai-default-indigo` | **Solid** use of 7 “default LLM accent” hexes (not only gradients) | `205` |
| **P0** | `emoji-icon` | 17 slop emoji appearing in `<h*>` / `<button>` / `<li>` / `class*="icon"` | `228` |
| **P0** | `left-accent-card` | Rounded card + left colored border (classic “AI dashboard tile” shape) | `245` |
| **P0** | `sans-display` | h1/h2/h3 `font-family` lands on Inter/Roboto/Arial/`-apple-system`/`system-ui`/SF Pro | `259` |
| **P0** | `invented-metric` | “10× faster” “99.9% uptime” “3× more productive” and 5 such sentence shapes | `272` |
| **P0** | `filler-copy` | `lorem ipsum` / `feature one\|two\|three` / `placeholder text` / `sample content` | `287` |
| **P0** | `scroll-into-view` | Used `Element.scrollIntoView()` | `296-304` |
| **P0** | `slide-theme-missing` | Deck has `.slide` but missing light/dark/hero theme classes | `465-479` |
| **P1** | `all-caps-no-tracking` | `text-transform: uppercase` but tracking <0.06em (including inline style) | `306-387` |
| **P1** | `external-image` | unsplash / placehold.co / placekitten / picsum / loremflickr and similar external placeholder images | `389-403` |
| **P1** | `raw-hex` | Naked hexes **over 12** outside `:root{}` in the first `<style>` | `405-429` |
| **P1** | `accent-overuse` | `var(--accent)` appears **>6 times** in body outside `<style>` | `431-446` |
| **P1** | `slide-rhythm` | 3 consecutive slides of the same theme (visual fatigue) | `480-506` |
| **P2** | `missing-section-anchor` | `<section>` missing `data-od-id` or `data-screen-label` | `448-463` |

### 13.2 Three checks that actually have engineering content

**① ALL CAPS tracking: token-aware + theme resolution + font-size conversion**

The naïve implementation is `/letter-spacing:\s*([\d.]+)em/` grabbing a number. What OD does is much more complex (`lint-artifact.ts:306-358`, `636-720`, `805-892`):

1. `extractCssTokens(html)` first collects `--name: value` declarations of **every scope** (`lint-artifact.ts:805-843`)
2. `buildResolvedThemes(scopes)` combines “global theme scopes” (`:root`, `[data-theme=…]`, etc., see `GLOBAL_THEME_ATTRIBUTES`, `lint-artifact.ts:974-979`) into multiple theme sets
3. `resolveCssVars(body, tokens)` recursively resolves `var(--x)`, **max depth 4** (`VAR_RESOLVE_MAX_DEPTH = 4`, `lint-artifact.ts:844`)
4. `resolveFontSizePx(decls)` converts `font-size` in the same rule to px (`ROOT_FONT_PX = 16`), because `letter-spacing: 1px` is enough on 12px type, far from enough on 48px type

So `letter-spacing: var(--caps-tracking)` is judged **by the literal value of that token in each theme set**, not treated as “not set.”

**② A real regex lesson (`lint-artifact.ts:328-341`)**

That comment is worth citing in full, because it is a classic case of “one wrong character class in a regex causing a false negative”:

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

One character-class difference, and ALL-CAPS headings inside `@media` escape the check.

**③ Three layers of “false-positive” defense**

- **Strip HTML comments** (`lint-artifact.ts:124-127`): comments often have teaching examples (“paste a `<section class="slide">` here”); without strip, false reports
- **Strip CSS comments** (`lint-artifact.ts:322-327`): `/* .eyebrow { text-transform: uppercase; } */` the browser does not render, but a rule-shape regex will match
- **`declarationLaundersIndigo` / `isTokenShapedDeclaration`** (`lint-artifact.ts:935-958`): distinguish “define indigo as a token” from “directly use indigo as accent”

**④ Thresholds all have reasons** (`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.

The number 12 is not made up; it is reverse-derived from “mobile-frame seed templates legitimately use 8–10.”

### 13.3 Feedback loop: `<artifact-lint>` system reminder

`renderFindingsForAgent()` (`lint-artifact.ts:519-537`) renders findings into a stretch of system reminder pasted back to the 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>
```

Three details: **sorted by severity** (P0 first), **explicitly require re-emitting a corrected edition rather than writing an explanation**, **each carries `fix` not only `message`**—an error given to an agent must come with a repair action, otherwise it will guess.

### 13.4 An honest boundary

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

That is: a P0 hit **does not stop landing on disk**. This is a reasonable trade-off—hard-blocking would turn “the model cannot fix it in a death loop” into “the user gets nothing.”

> 🧠 **One sentence**: `lint-artifact.ts` is something unique among these ten projects. Other projects check “tool-output correctness”; OD checks **aesthetic quality programmatically**. It binds `craft/anti-ai-slop.md` that human-readable rulebook and a 1 000-line enforcement code into a pair with the “keep this list in sync with…” that keeps appearing in comments.

---

<h2 id="ch14">Chapter 14 · Quality gate 2: Design Jury five-juror review theater</h2>

Product name **Design Jury**, internal code name **Critique Theater** (`docs/critique-theater.md:8-13`: code path `apps/daemon/src/critique/`, web side `components/Theater/`, SSE channel `critique.*`, env vars `OD_CRITIQUE_*`; **the user-visible name comes from a single i18n key `critiqueTheater.userFacingName`, so changing the product name does not require touching code**).

### 14.1 Five jurors and weighted composite

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

| Role | What they score | v1 weight |
|---|---|---|
| **Designer** | Layout, composition, hierarchy—“does this thing look good and balanced” | **0.0** |
| **Critic** | Does it actually satisfy the brief; contrast, type weight, readability | **0.4** |
| **Brand** | Token compliance, tone, brand-color use | **0.2** |
| **Accessibility** | WCAG, focus rings, semantic structure, alt text | **0.2** |
| **Copy** | Tone, brevity, error-copy quality | **0.2** |

Default config at `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,
```

**Why is Designer weight 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.

Keep the seat, weight to zero—let qualitative opinion enter the record, but do not let subjective aesthetics block ship. This is a very mature product decision.

**Config validation with epsilon tolerance** (`critique.ts:48-54`):

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

The comment explains: “Small epsilon tolerance so a fractional threshold that rounds up against an integer scale (e.g. 8.0 with floating-point slack) still validates.”—textbook floating-point comparison.

### 14.2 One CLI session, one transport

`docs/critique-theater.md:76-82` is the most critical point of this design:

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

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

Contrast: many multi-agent review systems open five processes/five sessions; the result is five copies of context that do not know each other, scores that contradict themselves, and auth, env vars, logs all have to be copied five times. OD’s practice eliminates these problems in one stroke.

### 14.3 Convergence loop

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

After settlement, fold into a badge, five states (`docs/critique-theater.md:151-160`): `Shipped` / `Below threshold` / `Timed out` / `Interrupted` / `Degraded`.

**`Interrupted` copy is written separately** (`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 Four-level switch parser

`docs/critique-theater.md:88-110` defines four layers, priority high to low:

| Priority | Layer | Note |
|---|---|---|
| 1 (highest) | **Skill-level `od.critique.policy`** | `required` force on, `opt-out` force off, `opt-in` only on at M2+ |
| 2 | **Project-level override** | Web setter writes `localStorage` (in-session UI) + **read-merge-write** on `PATCH /api/projects/:id` |
| 3 | **`OD_CRITIQUE_ENABLED` env var** | Advanced users / CI fixtures |
| 4 (lowest) | **Gray-stage default** | M0/M1 = false, M2 = `opt-in` skills true, M3 = fully on |

Layer 2’s read-merge-write has a detail against data loss (`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.**

**Skill author’s rule of thumb** (`docs/critique-theater.md:134-136`): skills that produce deterministic artifacts (e.g. `od-export-pdf`) usually `opt-out`; those that generate brand-new design output (`magazine-poster`, `saas-landing`) set `required`.

### 14.5 Degradation and adapter conformance

Five degradation reasons (`critique.ts:76-83` + `docs/critique-theater.md:185-191`):

| Reason | Cause | Disposition |
|---|---|---|
| `malformed_block` | The adapter’s `<CRITIQUE>` block the parser does not recognize | Run conformance test `tests/critique-conformance.test.ts` |
| `oversize_block` | Exceeds `parserMaxBlockBytes` (256 KB) | Usually the model ran away; retry or add budget |
| `adapter_unsupported` | That adapter is marked `critique:degraded`, **24h TTL** | Wait for TTL expiry, or call `clearDegraded(adapterId)` |
| `protocol_version_mismatch` | Adapter protocol version old | Upgrade the adapter or pin protocol negotiation |
| `missing_artifact` | Run ended but no `<artifact>` body | “Almost always a prompt bug; check the skill template.” |

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

**Condition for M3 full open** (`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.**

A feature across 25 third-party CLIs, using “14 consecutive days ≥90% conformance” as the full-open switch—this is very hard engineering discipline.

### 14.6 Replayable review

`docs/critique-theater.md:162-177`: each run writes a structured `.ndjson` (optional `.gz`) record. The Replay button hangs a read-only Theater that plays back at a selected speed: `Instant` (flush all events synchronously) / `Live` (original rhythm) / `{ intervalMs: N }` / `Paused` (after pause continue from the cursor, **does not re-flush already-sent events**). The replay surface can be keyboard-dragged: `J`/`K` jump by round, `Esc` exit.

---

<h2 id="ch15">Chapter 15 · Plugins and atoms: <code>atoms</code> + <code>pipeline</code> + a closed <code>until</code> vocabulary</h2>

### 15.1 Plugin minimum shape

`README` + `plugins/spec/SPEC.md`: a plugin must have `open-design.json`, plus **payload decided by type**:

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

Core fields: `specVersion` (currently `1.0.0`) · `name` (stable 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[]`.

**Minimum-capability declaration principle**: “**declare the minimum** — a restricted install grants only `prompt:inject` by default.”

**Six levels of official plugins** (measured directory counts):

| Level | Count | Content |
|---|---|---|
| `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** | One-shot image prompts |
| `video-templates/` | **64** | HyperFrames / Seedance / Veo motion templates |
| `design-systems/` | **143** | Brand `DESIGN.md` packed as plugins |
| `atoms/` | **13** | Reusable UI fragments + atom capability bodies |
| `examples/` | **183** | Remixable reference output |

### 15.2 Atom catalog: capability units the daemon exposes to plugins

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

Implemented atoms (`docs/atoms.md:26-48`, source of truth `apps/daemon/src/plugins/atoms.ts`):

| id | What it does | Applicable task kinds |
|---|---|---|
| `discovery-question-form` | Turn-1 questionnaire | new-generation, tune-collab |
| `direction-picker` | 3–5 direction choices before lock | new-generation, tune-collab |
| `todo-write` | TodoWrite-driven plan | all |
| `file-read` / `file-write` / `file-edit` | File ops on project cwd | all |
| `research-search` | Tavily-backed shallow research | new-generation |
| `media-image` / `media-video` / `media-audio` | Generate media via configured provider | new-generation, tune-collab |
| `live-artifact` | Create/refresh live artifact | new-generation, tune-collab |
| `connector` | Composio connector tool calls | new-generation, tune-collab |
| **`critique-theater`** | Five-dimension panel review, **emits the `critique.score` signal that drives convergence** | all |
| `code-import` | Clone/read an existing repo | code-migration |
| `design-extract` | Extract design tokens from source / Figma / screenshots | code-migration, figma-migration |
| `figma-extract` | Extract Figma node tree + tokens + assets | figma-migration |
| `token-map` | Map extracted tokens onto the active design system | code-migration, figma-migration |
| `rewrite-plan` | Long-run multi-file rewrite plan | code-migration, tune-collab |
| `patch-edit` | Small-step file patches | code-migration, tune-collab |
| `build-test` | Run build/typecheck/tests, produce a convergence signal | code-migration |
| `diff-review` | Render the rewrite as a reviewable diff | code-migration, tune-collab |
| `handoff` | Push the artifact to downstream (cli / cloud / desktop) | tune-collab |

### 15.3 Pipeline execution: five steps + one audit line

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

An honest note (`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.

That is: atoms like `file-write` whose actual work happens inside the CLI, the daemon cannot observe the real result, can only give a permissive signal. The docs do not pretend it can observe everything.

### 15.4 Closed `until` vocabulary

`docs/atoms.md:74-87` defines six signals usable for stage-convergence judgment:

| Signal | Who emits it |
|---|---|
| `critique.score` | `critique-theater` atom |
| `iterations` | Built-in per-stage counter |
| `user.confirmed` | When a `confirmation` GenUI surface is parsed |
| `preview.ok` | live-artifact preview pipeline |
| `build.passing` | build-test’s build/typecheck gate |
| `tests.passing` | build-test’s test gate |

**The key sentence** (`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.

This is a very important safety + maintainability decision. If marketplace plugins can write arbitrary JS as a convergence condition, you have to sandbox it, audit it, worry about infinite loops. A closed vocabulary limits “what a plugin can express” to semantics the daemon can guarantee; the cost is limited expressiveness—but what you buy is **the plugin marketplace can be open-install**.

### 15.5 Atom promotion path

`docs/atoms.md:89-100` defines a road from “external plugin” to “first-class atom”:

1. First implement the atom **out of tree as a plugin** per spec §22.5
2. After `SKILL.md` / MCP tools / pipeline shape stabilize, add a built-in atom at `plugins/_official/atoms/<id>/`, append a line to `FIRST_PARTY_ATOMS`, **and register a worker when the daemon has a real observable signal**
3. In the same PR update this document and the spec’s §10 / §21 / §23 tables
4. Afterwards reachable via `od.pipeline.stages[*].atoms[]`, `GET /api/atoms`, `od atoms list/show/info`, `od plugin doctor`

---

<h2 id="ch16">Chapter 16 · Security boundary: loopback / SSRF / desktop HMAC / <code>.od-skills</code> copy</h2>

`docs/architecture.md:245-261` lists seven boundaries. Split them one by one and look at the implementation:

### 16.1 Default loopback; public deploy needs an explicit three-piece kit

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

The README adds: LAN exposure needs explicit `OD_BIND_HOST` **plus** `OD_ALLOWED_ORIGINS`. **Connector credentials and live-artifact preview routes stay loopback-only no matter what.**

### 16.2 SSRF: default-block intranet; opt-out is strict exact match

The README stretch is the finest SSRF discussion in the whole repo:

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

Self-hosted intranet gateways (LiteLLM, Ollama on a VPN `10.x`) are allowed through `OD_ALLOWED_INTERNAL_HOSTS`; the rules are extremely strict:

| Property | Note |
|---|---|
| **Strict opt-in** | Default empty |
| **Exact host match** | **Does not** do subdomain/substring match |
| **Format-tolerant** | Accepts `host:port` or a full URL, reduced to hostname; IPv6 must have brackets `[fd00::1]` |
| **Scope-limited** | **Only** acts on provider endpoints you yourself configured (connect test, model discovery, BYOK chat) |
| **Does not loosen downstream** | **Deliberately does not** loosen download URLs returned in upstream responses—those stay blocked |
| **Bad items discarded** | Malformed entries, or **CIDR notation (unsupported)** are discarded and warned, not silently trusted |
| **Trust-transfer known** | “Allowlisting a hostname trusts whatever it resolves to; allowlist the resolved IP instead if you want the DNS-resolved address re-checked.” |

The last is especially important: it **explicitly says** allowing a hostname means trusting the DNS resolution result (DNS-rebinding risk), and gives a stricter alternative. This kind of “state residual risk clearly” documentation is worth far more than “we are very safe.”

### 16.3 Desktop trust: short-lived single-use HMAC token

`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: 之后每次文件访问都对该外部根做安全路径解析
```

The key sentence: **“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.”**—this marker is server-controlled; ordinary project create/update requests **cannot forge it**. Implementation in `apps/daemon/src/desktop-auth.ts` and `import-export-routes.ts`.

### 16.4 Sandboxed iframe: opt into only the minimum features needed

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

Preview URL vs srcDoc decision is owned separately by `apps/web/src/components/file-viewer-render-mode.ts` (`docs/architecture.md:164-169`). To use inspect, comment selection, palette/edit/tweak those **host-integration** capabilities, you must walk the srcDoc path. When switching render mode **both frames stay mounted** to avoid reload flicker; the message handler **validates the sender iframe**; signals that need to come from the active frame are **checked against the active window again**.

### 16.5 Spawn permissions: honestly admit this is “trusted agent execution”

The frankness of `docs/agent-adapters.md:442-465` this section deserves praise. It first explains why:

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

Then **lists item by item what dangerous flag each CLI is fed**:

| CLI | Headless permission posture |
|---|---|
| Claude | `--permission-mode bypassPermissions` |
| Cursor | `--force` + capability-gated `--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`, **and only when help probe confirms the flag exists** (old versions do not give unknown options) |
| Codex | macOS/Linux default `workspace-write` + network; Windows/WSL or explicit `OD_CODEX_SANDBOX=danger-full-access` uses `danger-full-access` (because the workspace-write path there cannot hold up the required shell execution) |

The conclusion is unambiguous:

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

**Why must it be this way?** `docs/agent-adapters.md:314` gives the reason: “the daemon runs agent CLIs **without a TTY**, so it must not rely on an interactive tool-approval prompt to make progress.” No TTY means no interactive approve; interactive approve would hang the run dead.

> ⚠️ **This is OD’s largest security trade-off**, and where it differs from every other project in this series: OpenWorker has five permission modes and inbox hang, CodeWhale has safe-by-construction, nanobot has a bwrap sandbox—OD **actively gives up the permission gate**, cedes it entirely to the hosted CLI, then uses the docs to say this clearly. The reason is self-consistent (no TTY + delegated policy), but the user must know what they are authorizing.

### 16.6 Windows command-line length: triple guards

The stretch at `docs/agent-adapters.md:345` (DeepSeek adapter) is the longest bullet in the whole document, about a very concrete cross-platform hell. DeepSeek’s clap declares `prompt: String` as a required positional, no `-` stdin sentinel, so the prompt can only enter argv. So `/api/chat` put on **three complementary guards**:

| Guard | Timing | What it checks |
|---|---|---|
| `checkPromptArgvBudget` | **Before bin resolve** (fast) | Raw assembled prompt byte count vs `maxPromptArgBytes` (DeepSeek declares 30 000) |
| `checkWindowsCmdShimCommandLineBudget` | After `buildArgs` | When the resolved is a Windows `.cmd`/`.bat` shim, recompute the full command line of `cmd.exe /d /s /c "<inner>"` with **the same per-arg quote-doubling rules as the platform layer** |
| `checkWindowsDirectExeCommandLineBudget` | After `buildArgs` | When the resolved is a non-shim Windows install (e.g. cargo-built `deepseek.exe`), recompute with **libuv’s `quote_cmd_arg` rules** (each `"` becomes `\"`, backslashes adjacent to quotes double) |

The two Windows guards are **mutually exclusive** on a given resolve: the cmd-shim guard covers `.cmd`/`.bat`, the direct-exe guard covers the rest. Together the three catch the case “**raw byte count did not overflow, but a quote-dense prompt (code blocks, JSON-shaped skill seeds) after expansion exceeds CreateProcess’s 32 767 character `lpCommandLine` cap**.”

All three emit the same actionable `AGENT_PROMPT_TOO_LARGE` SSE error, telling the user “reduce skill/design-system context, shorten the conversation, or switch to an adapter that supports stdin.” And all three have unit tests (over-long + short-prompt branches, quote-dense regressions of both Windows paths, mutual-exclusion checks), “so the guards can't silently regress.”

> 🧠 **One sentence**: this stretch is the complete bill of “how much it costs to support a CLI that refuses to read stdin.” It also explains why that comment in `claude.ts` writes all three numbers `MAX_ARG_STRLEN ~128 KB` and `CreateProcess ~32 KB / .cmd shim ~8 KB`—these are pits stepped in.

---

<h2 id="ch17">Chapter 17 · Feature leftovers</h2>

### 17.1 The `od` CLI and MCP server: turn OD into someone else’s tool

OD ships three shapes: **skills, CLI, MCP server**. One-line install (README):

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

After install, in any 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
```

**Why MCP rather than export zip?** The README’s reason is practical: “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.**”

**Security model**: default read-only, daemon binds `127.0.0.1`, SSRF intercepted at the proxy edge.

**A real name collision**: on macOS / WSL2 `/usr/bin/od` is the system’s octal-dump tool, and will cover Open Design’s `od` on PATH. The README in three places reminds the user to use the absolute-path fragment in **Settings → MCP server**. `install.sh`’s reason for existing is also practical: “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 proxy: can run without a CLI installed

`POST /api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream`—paste `baseUrl` + `apiKey` + `model` and you have the same loop, no spawn. Presets for OpenAI, Atlas Cloud, Anthropic, Azure OpenAI, Google Gemini, Ollama, LM Studio, vLLM, and any OpenAI-compatible endpoint.

But note **BYOK OpenCode** at `docs/agent-adapters.md:206-218`:

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

Early versions probably really implemented a “direct Anthropic + implement Read/Write/Edit ourselves” fallback loop in the daemon. Now it is thoroughly deleted—because that violated the fundamental claim “we do not implement an agent loop.” **This is an architecture regression worth respecting.**

### 17.3 No cross-agent fallback chain

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

Only two much narrower rules:
- **Pre-run default** (when `agentId` is omitted): use the configured agent if it is currently available, otherwise the first available def. “This chooses an agent **before** a run starts; it is **not** failure recovery.”
- **Expired-session recovery**: when a stored native session expires, the daemon can clear **the same agent**’s stale session and re-seed with the full transcript. “That recovery **never changes agent families**.”

This is a counter-intuitive but correct product decision: **auto-switching agent would quietly change billing, auth, output style**; the user would have no idea who just made that artifact.

### 17.4 Live Artifact vs Normal Artifact

The term distinction at `CONTEXT.md:11-25` is important:

| Concept | Definition | Avoid calling it |
|---|---|---|
| **Normal Artifact** | A project design output represented by one **artifact entry file** + one **artifact manifest** | live artifact, generic file upload |
| **Live Artifact** | A refreshable project design output, stored as a live-artifact record, with source data and preview state | normal artifact, static artifact |
| **Artifact Entry File** | The main project file that opens/renders a normal artifact | support file, asset, sidecar file |
| **Artifact Manifest** | Sidecar metadata that marks a project file as a normal artifact, recording kind / renderer / exports / entry | live-artifact document, project metadata |
| **Active Project** | The project the user most recently interacted with in the OD UI; MCP tools can use it when no project is specified | latest project, default project |

`CONTEXT.md:100-107` even gives a **sample dialogue** and a **resolved-ambiguity record**:

> **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’s open-source agent-native video framework, integrated as a first-class citizen. The agent writes **HTML + CSS + GSAP**; HyperFrames uses **headless Chrome + FFmpeg** to render a deterministic MP4. The repo ships **11 HyperFrames templates + 39 Seedance prompts**. OD-specific render workflows (composite cache, sandbox-exec workaround, MP4-as-chip) live in `design-templates/hyperframes/`.

### 17.6 Export matrix

| Format | How it is implemented |
|---|---|
| **HTML** | Single file, all assets inlined |
| **PDF** | Browser print, deck-aware |
| **PPTX** | Agent-driven skill (`pptx-html-fidelity-audit` skill has 10 files dedicated to fidelity audit) |
| **ZIP** | Archive |
| **Markdown** | — |
| **MP4** | HyperFrames |

### 17.7 mock agent: regression tests that do not burn quota

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

This is a necessity of maintaining 26 adapters—really running 25 CLIs every time you change a parser is impossible.

---

<h2 id="ch18">Chapter 18 · Wrap-up: three most distinctive designs and three trade-offs</h2>

### 18.1 Three most distinctive designs

#### ① The adapter is a data spec, not a class—press the cost of “supporting N CLIs” from O(N) to O(1)

In `RuntimeAgentDef` **45 fields, only `buildArgs` one function, and it is pure**. Probe, launch, invoke, cancel, parse all live in the shared engine. Result:

- Adding a CLI of a known wire format = **one object literal + one line in the registry**; the smallest def is only **629 bytes** (`defs/kilo.ts`).
- Nine ACP adapters share one transport.
- **Zero per-agent branches** in the engine—the sentence at `docs/agent-adapters.md:514` “The engine is agent-agnostic: it iterates `AGENT_DEFS` and reads fields” can be verified against code.

**Its restraint is equally key**: the contract has **no** `nativeSkillLoading`, **no** `skillInjectionStrategy`, **no** `capabilities()` method, **no** `surgicalEdit` feature-gate table. Every temptation to stuff behavior into the def is explicitly refused by the docs.

#### ② Turn “aesthetic quality” into a programmatic gate—`lint-artifact` + Design Jury two layers

This is a capability **unique among the ten projects in this series**. Other projects check “is the tool call correct”; OD checks “does this thing look like AI slop”:

- **Layer 1 `lint-artifact.ts` (1 000 lines)**: 16 rules, with CSS-variable recursive eval (depth 4), theme-scope resolution, font-size-converted tracking judgment, three-layer false-positive defense. The rule table and `craft/anti-ai-slop.md` are bound into a pair with “keep in sync” in comments.
- **Layer 2 Design Jury**: five jurors score in **the same CLI session** as five turns, weighted composite, ships only at ≥8.0/10, at most 3 rounds, timeout/miss-the-bar walks `ship_best`. Across 25 third-party CLIs uses “14 consecutive days ≥90% conformance” as the full-open switch.

And **neither layer hard-blocks**: a P0 hit does not stop landing on disk; a review that misses the bar walks ship_best. This is the mature trade-off of “there is a gate, but it does not lock the user dead.”

#### ③ Prompt cache-band ordering—take hermes’s “prefix is sacred” one step further

`composeSystemPrompt` splits 20+ fragments by **change frequency** into four bands (globally static → session-stable → project-stable → turn-volatile), and:

- The **stability of the trigger signal decides the block’s position**: a metadata signal fixed at project creation can sit in the project-stable band; a text signal that may flip mid-conversation must be pushed to the turn-volatile suffix, because “an early insert would break the cached prefix for **every section after this line**.”
- Matching `describeStablePromptCache()` does **hit attribution**; on a miss diffs segment by segment which one drifted, and deliberately does not report full-set change in a “no baseline” situation so as not to drown the signal.
- The slim variant also turns the direction library into “index + `od tools directions --id` pull-on-demand”—but **only under the filesystem execution profile**, because the text_artifact profile has no tools to dereference the index.

### 18.2 Three trade-offs you must know

#### Trade-off 1: actively giving up the permission gate

OD feeds every CLI the most dangerous non-interactive flag: `bypassPermissions` / `dangerous` / `--yolo` / `--allow-all-tools` / `--dangerously-allow-all` / `--auto`. The reason is self-consistent (no TTY, and policy should be enforced by the CLI itself); the docs are also frank (“treat these runs as trusted agent execution”); but the fact is:

**In this series, OpenWorker has five modes + inbox hang, nanobot has a bwrap sandbox, CodeWhale has safe-by-construction, Claude Code has a permission gate—OD has none of them.** Its security budget is all spent on the **boundary** (loopback, SSRF, HMAC, sandboxed iframe, `.od-skills` copy), not on **execution**.

For personal local use, this is reasonable; for team shared deploy, this needs a very serious evaluation.

#### Trade-off 2: two prompt assemblers must stay byte-sync

Daemon-side `prompts/system.ts` (2 075 lines) and contracts-side `prompts/system.ts` (1 064 lines) are **two independently implemented copies of the same semantics**. The repo has at least six comments reminding “keep both in sync” “keep this whole block BYTE-IDENTICAL.”

This is the tax paid for “the same design flow must serve both local CLI execution and BYOK direct connect.” In theory you could extract a shared layer, but the daemon side has to read the filesystem, assemble craft, connect critique; the contracts side must stay pure (`apps/daemon/AGENTS.md:96`: “Keep `packages/contracts` pure. Do not move daemon-only Node, SQLite, Express, filesystem, or process types into contracts.”). This drift risk is **known and explicitly managed**, but it is real technical debt.

#### Trade-off 3: the content library’s volume itself is a maintenance burden

151 design-system packages × at least 3 files each, 164 skills, 115 rendering templates, 277 official plugins, 183 examples, 107 media prompt templates—together 2 246 Markdown, 2 493 HTML. Repo 322 MB.

**Quality guards** for this content are already written in quantity (`pnpm guard`, `pnpm lint:craft`, package quality guards, derived-file consistency checks, token contracts, component fixtures, source evidence, preview coverage), but the speed of content rot and the speed of content growth are the same order of magnitude. A three-month 82k-star growth curve means community contribution will keep pouring in—**whether this set of guards can hold is this project’s largest uncertainty in the coming year.**

### 18.3 Its place among these ten projects

| Dimension | The other nine coding/task agents | **Open Design** |
|---|---|---|
| **Main loop** | Each implements its own | **Does not implement**; delegates to 25 CLIs |
| **Tool system** | Defines 40–74 tools itself | **Zero**; uses the hosted CLI’s native tools |
| **Permission model** | From safe-by-construction to five modes | **None**; all delegated + boundary defense |
| **Context engineering** | Compact, prune, dual-track memory | **Layered assembly + cache bands** (20+ layers) |
| **Quality assurance** | Tests pass / compile passes | **Aesthetic linter + five-juror review** |
| **Extension mechanism** | Plugins / MCP / skills | **Four planes**: skills + templates + design-systems + craft |
| **Deliverable** | Code changes | **HTML/PDF/PPTX/MP4 real files** |
| **Product shape** | CLI / TUI / IDE plugin | **Local-first desktop app + MCP server + Docker** |

> 🧠 **Last sentence**: if OpenWorker answers “how to let a model finish cross-app work like a colleague,” Open Design answers another question—**“there is already a pile of very strong coding agents; how do we make them together become a designer?”** The answer it gives is not building another agent, but building a whole **filesystem they can all read** (skills + templates + brand + craft), plus a layer of **sockets they can all be described by the same data spec**, plus two **aesthetic gates they all must pass**.
>
> Whether this answer is right, three months 82k stars have already given a preliminary market judgment. But its real engineering value is: **it proved that the “adapter-as-data” road can hold 25 heterogeneous CLIs, and proved that “design quality,” this thing that looks purely subjective, has a substantial part that can be programmatically enforced.**

---

## Appendix A · Source-reading index

| Want to see | Go to this file |
|---|---|
| Adapter contract (45 fields) | `apps/daemon/src/runtimes/types.ts:101-253` |
| 26 defs’ registration and dedupe invariant | `apps/daemon/src/runtimes/registry.ts:30-76` |
| A complete-def sample | `apps/daemon/src/runtimes/defs/claude.ts` (98 lines) |
| Probe pipeline | `apps/daemon/src/runtimes/detection.ts:238-318` |
| Launch-path resolve (probe and execute must be consistent) | `apps/daemon/src/runtimes/launch.ts` |
| `<artifact>` extract from a naked-text stream | `apps/daemon/src/runtimes/plain-stream.ts` (473 lines) |
| Claude stream-json parse | `apps/daemon/src/runtimes/claude-stream.ts` (25.9 KB) |
| ACP transport (shared by nine adapters) | `apps/daemon/src/agent-protocol/acp/session.ts` (40.7 KB) |
| Execution-profile map (249 bytes) | `packages/contracts/src/execution-profile.ts` |
| Prompt assembly (daemon side) | `apps/daemon/src/prompts/system.ts:791-1370` |
| Prompt assembly (BYOK-side mirror) | `packages/contracts/src/prompts/system.ts:256-496` |
| Three hard rules + design philosophy | `packages/contracts/src/prompts/discovery.ts:25-354` |
| Designer charter (adapted from claude.ai/design) | `packages/contracts/src/prompts/official-system.ts:11-163` |
| Cache-hit attribution | `apps/daemon/src/runtimes/chat-prompt-inputs.ts:394-442` |
| Anti-AI-taste linter | `apps/daemon/src/lint-artifact.ts` (1 000 lines) |
| Review-theater config and schema | `packages/contracts/src/critique.ts` |
| Review orchestrator | `apps/daemon/src/critique/orchestrator.ts` (37.8 KB) |
| Skill staging (security-fix scene) | `apps/daemon/src/cwd-aliases.ts:1-60` |
| Skill registry and mode normalize | `apps/daemon/src/skills.ts` (42 KB) |
| Atom catalog and pipeline | `apps/daemon/src/plugins/atoms.ts` · `pipeline.ts` · `pipeline-runner.ts` |
| Desktop-trust HMAC | `apps/daemon/src/desktop-auth.ts` |
| Preview render-mode decision | `apps/web/src/components/file-viewer-render-mode.ts` |

## Appendix B · Key document index (this repo’s documentation quality is worth learning on its own)

| Document | Why it is worth reading |
|---|---|
| `docs/agent-adapters.md` (37 KB) | The most important document in the whole repo. Contains the contract, probe strategy, 12 adapters annotated one by one, authorization boundary, open questions |
| `docs/architecture.md` (13 KB) | Contains a rare “historical negative note”—lists early drafts overturned by the implementation |
| `docs/skills-protocol.md` (17 KB) | Skill format, discovery priority, seven modes, craft subscribe, staging rules |
| `docs/critique-theater.md` (10.7 KB) | Five jurors, four-level switch, degradation-reason table, replay, conformance bar |
| `docs/atoms.md` (5.6 KB) | Atom catalog, pipeline parse, closed `until` vocabulary, promotion path |
| `docs/modes.md` (5.9 KB) | Deliberate misalignment of six create tabs × seven skill modes |
| `CONTEXT.md` (6.4 KB) | Domain-language dictionary; every term carries `_Avoid_` (synonyms you must not use) |
| `AGENTS.md` (37.9 KB) + each layer’s `AGENTS.md` | Repo ownership rules, data-root contract, dual-track rule |
| `craft/README.md` (6.3 KB) | The fourth axis’s thesis, two-level enforcement, runtime tolerant vs repo strict |
| `design-systems/README.md` (5.8 KB) | Package contract, derived files are cache, seven-H2 floor |

---

*This lecture is written against `nexu-io/open-design` commit `c893b60` (2026-07-28), checked file by file. Every `file:line` citation can be walked back to source; counts (file count, directory count, byte count) are computed by a full walk of the GitHub Git Tree API.*
