English lecture · source-traced

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

EN
lecture
source
file:line
no
embellishment
中文
sibling

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

Part 2

Chapter 1 · Project overview: an agent product that does not write an agent loop

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.


Part 3

Chapter 2 · Panorama: four processes · four content classes · one SSE

2.1 Process topology

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

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

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):

flowchart LR
    REQ["GET /api/skills"] --> SCAN1["① 扫描用户可写根
(daemon 数据根下)"] SCAN1 --> SCAN2["② 扫描内置根
skills/"] SCAN2 --> MERGE["合并:同名 id 用户条目
遮蔽内置条目(不删除)"] 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.


Part 4

Chapter 3 · Three run shapes and the single truth of the “data root”

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

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
(校验 + 边界限制,不拷贝)"]

Part 5

Chapter 4 · Adapter contract: RuntimeAgentDef is a data spec, not a class

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:

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:

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:

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

// 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):

    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):

    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:

    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):

      // 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):

    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.


Part 6

Chapter 5 · Probe and capability negotiation: why you must probe “the path that will actually be spawned later”

5.1 Probe pipeline

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

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


Part 7

Chapter 6 · Four stream formats: from claude-stream-json to ACP to naked text

6.1 Seven streamFormats, 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):

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

// 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):

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:

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


Part 8

Chapter 7 · Two execution profiles: filesystem vs text_artifact

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

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

flowchart TB
    RUN["一次生成"] --> PROF{"executionProfileFromStreamFormat
(def.streamFormat)"} PROF -->|filesystem| F1["CLI 用自己的 Write/Edit 原生工具
直接在项目 cwd 写文件"] F1 --> F2["文件事件推给文件工作区"] F2 --> F3["可预览文件 = 交付物"] F3 --> F4["助手以普通摘要收尾
❌ 禁止再输出 <artifact> 源码块"] PROF -->|text_artifact| T1["模型循环里没有文件系统工具"] T1 --> T2["唯一交付形态:一个完整的
<artifact> 源码块"] T2 --> T3["run 结束后 daemon 扫 stdout
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.”


Part 9

Chapter 8 · Input dispatch: six create surfaces × seven skill modes

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


Part 10

Chapter 9 · The heart: twenty layers of composeSystemPrompt and cache bands

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)

flowchart TB
    A["① PROMPT_INJECTION_RESISTANCE
(钉最前,后文不可覆盖)"] --> B B["② API_MODE_OVERRIDE
仅 streamFormat==='plain'"] --> C C["③ PLAN_MODE / CHAT_MODE 覆盖
按 sessionMode"] --> D D["④ examplePrompt / skipDiscoveryBrief 覆盖"] --> E E["⑤ UI locale 提示"] --> F F["⑥ DISCOVERY_AND_PHILOSOPHY(~3000 token)
媒体面 & Ask 模式跳过"] --> G G["⑦ 方向库 renderDirectionSpecBlock()
仅当没有活跃设计系统"] --> H H["⑧ 共享设备边框目录
仅当多目标项目"] --> I I["⑨ 身份与工作流宪章
renderOfficialDesignerPrompt()"] --> J J["⑩ 会话中澄清问题(question-form)"] --> K K["⑪ 个人记忆 + 意图网关 + 自检记分卡 + 规则提案"] --> L L["⑫ 用户级自定义指令"] --> M M["⑬ 项目级自定义指令(覆盖用户级)"] --> N N["⑭ ## Active design system
USAGE.md → DESIGN.md → import mode →
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 /
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:

flowchart LR
    Z1["① 全局静态
设计师宪章 · 注入抵抗
所有会话共享"] --> Z2["② 会话稳定
模式覆盖 · locale"] Z2 --> Z3["③ 项目稳定
设计系统 · 技能 · 元数据 · 平台"] Z3 --> Z4["④ 回合可变
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:

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.


Part 11

Chapter 10 · Three hard rules: question-form → brand branch → TodoWrite

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

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

    Note over A: RULE 1 · 第 1 回合
    U->>A: "帮我做一个 SaaS 落地页"
    A->>H: 一句短散文 +  + 停
    Note right of A: ❌ 不读文件 ❌ 不 Bash
❌ 不 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 档发 )

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):

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.


Part 12

Chapter 11 · Memory and the double loop: task-brief / verify-scorecard / rule-proposal

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.

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


Part 13

Chapter 12 · Four content planes: skills / design-templates / design-systems / craft

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


Part 14

Chapter 13 · Quality gate 1: lint-artifact programmatic anti-AI-taste

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.


Part 15

Chapter 14 · Quality gate 2: Design Jury five-juror review theater

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:

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:

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):

.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

flowchart TB
    ART["Agent 发出 <artifact>"] --> R1["Round 1:五位陪审员各自打分
(同一会话的五个回合)"] R1 --> COMP["composite = designer×0 + critic×0.4
+ 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.


Part 16

Chapter 15 · Plugins and atoms: atoms + pipeline + a closed until vocabulary

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:

flowchart TB
    M["插件 manifest
od.pipeline.stages[*].atoms[]"] -->|① 解析| PS["PipelineStage[]
plugins/pipeline.ts"] PS -->|② run 开始前| BODIES["解析内置原子指令体
plugins/atom-bodies.ts
← plugins/_official/atoms/<atom>/SKILL.md"] BODIES --> BLOCK["渲染成 ## Active stage 提示词块"] BLOCK --> RUN["③ 运行时 pipeline-runner.ts 走每个阶段"] RUN --> E1["发 pipeline_stage_started SSE"] E1 --> W["④ 向 atoms/registry.ts 的 worker 要
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

Part 17

Chapter 16 · Security boundary: loopback / SSRF / desktop HMAC / .od-skills copy

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:

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 + 规范化路径
拒绝落在自己托管存储内的导入 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.


Part 18

Chapter 17 · Feature leftovers

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):

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:

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.


Part 19

Chapter 18 · Wrap-up: three most distinctive designs and three trade-offs

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.


Part 20

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
Part 21

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.

↑