从零构建 路线 D Open Design 系

造一个把别人的 Agent 当引擎的设计宿主

这条路线最反直觉的一点:你一行 Agent 主循环都不写。你要写的是「插座 + 剧本 + 闸门」。 跟着「帮我们做一个 SaaS 落地页,用我们公司的品牌」这一句话,从白纸走到成品——每撞一堵墙,补一个零件。

0 行
你要写的主循环
~3 000 行
适配器层(接住 25 个 CLI)
~2 000 行
提示词组装器(真正的产品)
~1 000 行
审美 linter(独一份的能力)
起点

先看终点:一个「设计引擎宿主」长什么样

在写第一行代码前,先把要拼的东西看清楚。编程 Agent 和设计 Agent 宿主的分界,不在模型,而在这五件事

  1. 它自己不推理——推理是你机器上那个 claude / codex / cursor-agent 在干;
  2. 它的产物是给人看的,不是给编译器看的,所以「对不对」之外还有「好不好看」;
  3. 好看这件事有相当大一部分可以程序化执法(Tailwind indigo、两段式渐变、emoji 当图标、ALL CAPS 不加字距);
  4. 品牌是一份可以版本化的契约,不是一堆散落的 CSS 变量;
  5. 交付物是真实文件,能预览、能导出、能扔进 Cursor 继续写代码。
flowchart TB
    subgraph UI["① 你能看到的"]
        CHAT["聊天 + 问卷卡片"]
        FW["文件工作区"]
        PREV["沙箱 iframe 预览"]
    end
    subgraph HOST["② 宿主(你要写的全部代码)"]
        REG["运行时注册表
26 条适配器数据定义"] COMP["提示词组装器
20+ 层,按变化频率分带"] LINT["反 AI 味 linter"] JURY["五陪审评审剧场"] SSE["统一事件流(SSE)"] end subgraph CONTENT["③ 内容四平面(文件系统,可版本化)"] SK["skills/ 功能技能"] TP["design-templates/ 渲染模板"] DS["design-systems/ 品牌契约"] CR["craft/ 通用工艺规则"] end subgraph ENGINE["④ 引擎(不是你的代码)"] CLI["claude / codex / cursor-agent /
copilot / opencode / …"] end CHAT --> SSE SSE --> COMP COMP --> CONTENT COMP -->|组装好的系统提示词| REG REG -->|spawn,cwd = 项目工作区| CLI CLI -->|原生 Write/Edit 工具| FILES["项目文件(真实磁盘)"] CLI -->|stdout| SSE FILES --> FW --> PREV FILES --> LINT -->|artifact-lint 系统提醒| CLI FILES --> JURY -->|轮次摘要| CLI
图 1 装配全景。下面 17 步每一步都是往里填一个方块。注意 ④ 整块是借来的——推理全是别人的,产品全是你的。
装配进度追踪

读完一步、装好一个零件就点一下。进度存在浏览器本地,关掉页面也在。

01决定不写主循环——能说清这个决定带来的三个新问题
02适配器契约——6 个必填字段 + 一个纯 buildArgs
03探测流水线——探测和执行走同一条路径解析
04spawn + stdin 投递——绕开 E2BIG / ENAMETOOLONG
05四类解析器——统一事件集 + <artifact> 提取四道防坑
06两档执行画像——filesystem vs text_artifact
07品牌契约——三文件包 + 八层注入 + 品牌提取五步
08工艺第四轴——按需订阅 + 两级执法
09三条硬规则——问卷 → 品牌分支 → TodoWrite
10优先级管理——位置 / 显式仲裁 / 条件门控
11缓存分带——四带排序 + 命中归因
12审美 linter——16 条规则 + 三层假阳性防护
13评审剧场——五陪审员,一个会话
14记忆双环——三张卡 + 程序化执法
15扩展机制——原子 + 封闭 until 词汇表
16六道边界——loopback / SSRF / HMAC / iframe / 拷贝 / 命令行守卫
17产品化——三形态 + 数据根契约 + dual-track + MCP 服务端
00

场景登场:一句「给我们做个落地页」

我们的设计 Agent 只需要会干一件事(先把一件事干透):

「帮我们做一个 SaaS 产品落地页,用我们公司的品牌。」

这一句话里,藏着我们要撞的每一堵墙:

用户说的撞上的墙在哪一步补
「帮我做」谁来推理?我要不要自己写主循环?12
(机器上装了什么?)claude 吗?版本对吗?登录了吗?3
(提示词有 40 KB)argv 塞不下,Linux E2BIG、Windows ENAMETOOLONG4
(不同 CLI 输出格式不同)Claude 吐 JSONL、DeepSeek 吐裸文本5
(产物在哪?)有的 CLI 会写文件,有的只会吐文本6
「用我们公司的品牌」品牌怎么变成模型能吃的东西?7
(做出来一眼是 AI 拉的)通用排版常识不在任何一份品牌文档里8
(15 秒没反应,用户走了)首字节时间9
(规则互相打架)「turn 1 必须问方向」vs「已选设计系统别再问」10
(每回合几万 token)前缀缓存11
(紫色渐变 + 🚀 图标)审美质量的程序化执法1213
(用户第三次说「别用米色」)记忆14
(别人想加自己的模板)扩展机制15
(你把 --yolo 喂给了子进程)边界防御16
「导出成 PDF 给老板」产品化17
Part A

决定不写 Agent

最重要的一个决定,也是最容易做错的一个。

01

先做一次诚实的成本核算

方案 A:自己写循环。模型调用与重试、工具定义与解析、上下文压缩、权限确认、断点续跑、取消、成本统计、多 provider 兼容、prompt 缓存。≈ 8 000–20 000 行(对照本系列其他九个项目的规模)。写完之后大概率不如 Claude Code——那是别人全职团队做了一年的东西。
方案 B:调用用户已经装好的那个。探测、启动、参数构建、流解析。≈ 3 000 行

Thesis: The code agent space has already converged on strong implementations… Reimplementing another one is worse than talking to all of them. docs/agent-adapters.md:9

这个决定的连锁后果(必须提前想清楚)

选 B 不是省事,是换了一组问题

你不用做的你必须接受的
主循环、工具系统、上下文压缩你控制不了它怎么想——只能通过系统提示词影响
模型兼容、重试、成本统计你的权限模型没了——子进程的权限由它自己管
断点续跑你要处理 25 种不同的 stdout 格式
多 provider 抽象用户没装 CLI 你就跑不起来(所以还得有 BYOK 兜底)
最重要的一条推论 既然你控制不了推理,你唯一的杠杆就是上下文工程 + 输出闸门。所以这份教程后面 60% 的篇幅都在讲提示词和闸门——那才是你真正的产品
✅ 本步验收 你能用一句话回答「为什么我们不写主循环」,并且能列出这个决定带来的三个新问题。
02

适配器即数据:一个对象字面量描述一条 CLI

先看错误的做法

// ❌ 别这么写
abstract class AgentAdapter {
  abstract detect(): Promise<boolean>;
  abstract run(prompt: string): AsyncIterable<Event>;
  abstract cancel(): void;
}
class ClaudeAdapter extends AgentAdapter { /* 200 行 */ }
class CodexAdapter  extends AgentAdapter { /* 200 行 */ }
// × 25 = 5000 行,且每个都要自己实现取消、超时、错误分类

问题不在代码量,在行为漂移:25 个 run() 会有 25 种取消语义、25 种超时处理、25 种错误分类。半年后你会发现 Cursor 的取消不干净、Copilot 的超时没生效——因为没人能同时维护 25 份生命周期代码

正确的做法:把「怎么跟它说话」和「怎么调度它」彻底分开

// ✅ 一个纯数据对象 + 一个纯函数
type RuntimeAgentDef = {
  id: string;                            // 唯一键
  name: string;                          // 显示名
  bin: string;                           // PATH 上探测的可执行文件
  versionArgs: string[];                // 版本探测参数
  fallbackModels: RuntimeModelOption[];  // 探测不到模型时的静态兜底
  buildArgs: (prompt, imagePaths, extraDirs?, options?, ctx?) => string[];  // 唯一的函数,且是纯的
  streamFormat: string;                 // 引擎据此分派解析器
  // …~38 个可选字段,全部是数据
};
flowchart LR
    DEF["RuntimeAgentDef
(纯数据)"] --> E1["detection.ts
探测"] DEF --> E2["launch.ts
路径解析 + 环境"] DEF --> E3["invocation.ts
argv 组装 + spawn"] DEF --> E4["run 生命周期
取消 / 超时 / 错误分类"] DEF -->|streamFormat 分派| E5["4 个解析器之一"]
图 2 六个必填字段,一个纯函数,剩下全部由共享引擎做。

逐条讲为什么:以 Claude Code 的定义为样本

① fallbackBins
生态里会出现 argv 兼容的分叉
fallbackBins: ['openclaude']——issue #235 里用户只装了 OpenClaude。一行数组解决,不用让用户写 wrapper 脚本。
③ helpArgs
探对子命令的帮助
helpArgs: ['-p','--help'] 而不是 ['--help']——--add-dir--include-partial-messages 只出现在 claude -p 的帮助里(issue #430 的修复)。
④⑤ 能力门控
老版本不认识新 flag 会直接退出 1
capabilityFlags 是「帮助输出里的子串 → 能力键」,探测后写进内存 map,buildArgs 读它决定加不加。每个可选 flag 都要先探测再用。
⑥ 会话续跑
别每回合重拼整段历史
让 CLI 保留自己的会话,它就保住了工作记忆(读过哪些文件、改过什么、工具历史)。
buildArgs: (_prompt, _imagePaths, extraAllowedDirs = [], options = {}, ctx = {}) => {
  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 (ctx.resumeSessionId)   args.push('--resume', ctx.resumeSessionId);
  else if (ctx.newSessionId) args.push('--session-id', ctx.newSessionId);
  args.push('--permission-mode', 'bypassPermissions');
  return args;
},

注意 _prompt 前面那个下划线——提示词根本不进 argv。理由见第 4 步。

三种「会话续跑」风格要分清

字段谁生成 session id例子
resumesSessionViaCli你生成,告诉 CLI 用它claude --session-id <uuid>
capturesSessionIdFromStreamCLI 生成,从流里报出来,你抓下来存codexthread.started.thread_id
resumesSessionViaAcpLoad从 ACP 会话拿 durable idAMR/Vela 用 session/load
⚠️ 一个必踩的坑:双份上下文导致问卷循环 如果某条 CLI 自己有会话记忆(比如 agy -c),你又同时把渲染的 web transcript 拼进用户消息,它会收到两份上一回合——包括它第 1 回合发出的 <question-form> 原文。模型会模式匹配这段原文,在第 2 回合再发一遍问卷,看起来像发现循环卡死了。

解法不是改提示词,是加一个 opt-out 标记跳过 transcript 注入。types.ts:70-84 记录了这次尸检。

注册表:81 行 + 一条加载期不变式

const ids = new Set();
for (const def of AGENT_DEFS) {
  if (ids.has(def.id)) throw new Error(`Duplicate agent definition id: ${def.id}`);
  ids.add(def.id);
}

那个 throw模块加载期执行。用户自定义的 profile 撞了内置 id → daemon 起不来,而不是安静地覆盖掉内置适配器。让错误尽早、尽响。

契约要克制:明确它不包含什么

这一点比「它包含什么」更重要。刻意没有nativeSkillLoading / skillInjectionStrategy(技能投递是共享行为)、capabilities() 方法(那个文件全文只有 131 字节)、surgicalEdit 特性门表(那会变成一张永远对不齐的能力矩阵)。

判断标准 如果一个字段描述的是「这个 CLI 长什么样」,放进定义;如果描述的是「遇到这种情况该怎么办」,放进引擎。
✅ 本步验收 新增一条使用已有 wire format 的 CLI,改动只应该是一个新文件 + registry 里一行,引擎代码零改动。Open Design 里最小的定义是 defs/kilo.ts629 字节
想看不同定义怎么长出不同 argv?去解析站的 LAB 01 适配器解剖台拨开关。
Part B

接住引擎

探测它、启动它、听懂它、接住它的产物。这四步就是那 ~3 000 行。

03

探测:怎么知道机器上有什么(以及一个必踩的坑)

flowchart TB
    S["probe(def, configuredEnv)"] --> R["① resolveAgentLaunch(def, env)
解析出「将来真正会被 spawn 的那个路径」"] R -->|解析不出| U1["不可用 + 可执行文件诊断"] R --> V["② 版本探测(在那个路径上)"] V -->|OS 级缺失/不可执行| U2["不可用 + 不可调用诊断"] V -->|能启动但拒绝 --version| OK1["✅ 可用,version = null"] V -->|成功| P OK1 --> P P["③ 三个后置探测并发 Promise.all"] --> C1["--help 能力表"] P --> C2["模型发现"] P --> C3["鉴权探针(仅当声明了)"] C1 & C2 & C3 --> OUT["DetectedAgent"]
图 3 探测流水线。版本探测必须先完成(它决定可用性),后三个互相独立、并发跑。
本步唯一真正重要的一条 探测和执行必须走同一条路径解析。

「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 PATHIf detection probes the shim but chat/run spawns the native binary, the UI incorrectly reports "not installed".」detection.ts:243-250

翻译:GUI 启动的 App 拿到的 PATH 是被系统精简过的(macOS 的 launchd 不读你的 .zshrc);nvm/fnm/mise 装的 CLI 常常是 shim,在那个精简 PATH 下跑不起来;但你的启动解析器可能有能力升级到打包的原生二进制。于是探测用 A 路径说「没装」,真跑用 B 路径其实能跑——用户看到红叉但功能是好的

规则:探测入口的第一件事必须是调用和 spawn 完全相同的那个路径解析函数

三个次要但会救命的细节

版本探测结果判定为什么
OS 级缺失 / 不可执行不可用真的没有
能启动,但拒绝 --version可用,version 留空有些 CLI 改过版本 flag 名,不该判死
成功可用 + version
  • 并发但有序:「a single agent's detection wall is max(help, models, auth) ≈ 5s rather than the sum ≈ 15s」
  • 每条适配器故障隔离:裸 Promise.all 会因一条拒绝而整体拒绝——一个坏掉的可执行文件不能清空整个选择器

鉴权:不猜

老做法是看 ~/.foo/ 目录在不在猜有没有登录,猜错的代价是把能用的 agent 标红。新规则:只有声明了 authProbe 的适配器才主动探测鉴权;没声明的 → authStatus 就是 unknown;真正的鉴权问题只从真实运行失败的错误文本里推断。探针必须是便宜、无副作用的 status/whoami 类命令。

探测的 UX

GET /api/agents?stream=1每探完一条发一个 SSE 事件,设置面板不用等最慢的 CLI 就能开始画卡片。不做 24 小时缓存——用户刚 npm i -g 装完,刷新就该看见。

✅ 本步验收claude 从 PATH 移走 → 卡片变灰但其他 25 条不受影响;用 nvm 装一个 shim 版 codex → 探测结果和实际能不能跑一致
04

起跑:spawn 子进程,提示词走 stdin 而不是 argv

先算一笔账

提示词片段量级
设计师宪章~4 KB
发现层 + 设计哲学~12 KB(约 3 000 token)
方向库(没选设计系统时)~6.7 KB
活跃设计系统(DESIGN.md + tokens.css + 组件清单)8–40 KB
craft 工艺规则(3 个 slug)~6 KB
活跃技能正文2–15 KB
合计30–80 KB 很常见
LinuxMAX_ARG_STRLEN单个 argv 条目限制在 ~128 KB → spawn E2BIG

WindowsCreateProcess整条命令行限制在 ~32 KB(通过 .cmd shim 只有 ~8 KB)→ spawn ENAMETOOLONG
规则:只要 CLI 支持 stdin,就走 stdinpromptViaStdin: true)。buildArgs 的第一个参数会是 _prompt——带下划线,因为根本不用。
字段投递方式何时用
promptViaStdin: true写进子进程 stdin默认首选
promptViaFile: true写进临时文件,路径通过 ctx.promptFilePathbuildArgsCLI 有显式的 prompt-file flag
(都不设)进 argvCLI 硬要位置参数(如 DeepSeek 的 clap 声明 prompt: String 必填)——要额外上守卫,见第 16 步

stream-json 输入格式:为什么要保持 stdin 打开

When set to 'stream-json' the daemon writes a single JSONL line wrapping the prompt as an Anthropic user message (so tool_result blocks can later be injected into the same stdin without re-spawning the child). types.ts:125-130

Stdin remains open so the daemon can forward additional user messages mid-turn, then is closed after a clean terminal turn_end/usage rather than at a mid-tool tool_use pause.

这条规则会咬你 如果你在看到 stop_reason: tool_use 时就关 stdin,Claude 会以为对话结束,回合中途断掉apps/daemon/AGENTS.md:113 专门写了一条守则:「do not close stdin on tool_use stop reasons」。

cwd 就是项目工作区 + 一个看门狗

spawn(resolvedBin, args, { cwd: projectWorkspaceDir, env: spawnEnv })

cwd 是执行根,不是沙箱(第 16 步会再强调一次)。子进程的 Write/Edit 直接落在这里,你的文件工作区监听这个目录的变化。

子进程可能长时间不吐字节(比如 Copilot 生成 deck 时的思考阶段)。所以要有基于 stdout/stderr/SSE 活动的不活跃超时,全局默认 10 分钟,适配器可以用 inactivityTimeoutMs 声明更长上限,操作者可以用环境变量覆盖(env 优先)。注释点破了本质:「The watchdog observes child stdout/stderr/SSE activity, not real CPU progress」——你观测的是「有没有说话」,不是「有没有在干活」

✅ 本步验收 构造一个 60 KB 的提示词,在 macOS + Windows 上都能正常跑完;在工具调用中途不会断流。
05

听懂它说话:四种流格式,一套统一事件

25 个 CLI 有 25 种 stdout?不。按 wire format 分类之后只有 7 种 streamFormat、4 类解析器。所有解析器输出同一套事件thinking / tool-call / tool-result / text-delta / file-write / error / done。UI 只认这套,不认底层格式。

九条 ACP 适配器共用同一个传输层 这就是「新增一条 ACP agent 只要一个 629 字节的对象」的原因。分类的价值不在少写代码,在于让边际成本趋近于零

plain 流:最弱的适配器要写最多的代码

裸文本流的 CLI 没有结构化的文件写入事件。约定是让它吐 Anthropic 风格的源码块,run 结束时扫 stdout 提取。听起来五分钟能写完,实际要 473 行,因为要绕开四个坑:

坑 1
Markdown 围栏里的假 artifact
模型解释「你应该这样写」时会把 <artifact> 放进代码围栏。天真的 indexOf 会把教学示例当真产物写盘。
解法:先算跳过区间。围栏逐行判断,行内反引号要匹配相同数量的连续反引号。
坑 2
前缀误判
<artifacts> 不是 <artifact>
return /\s/.test(
  text.charAt(idx + '<artifact'.length));
坑 3
属性值里的 >
title="A > B" 会被 indexOf('>') 截断。要用带引号状态机找开标签结尾。
坑 4
嵌套 / 未闭合
每次找到开标签,先探测下一个开标签的位置:如果它出现在当前开标签结束之前或闭标签之前,说明当前这个坏了,跳到下一个重来。一段畸形输出不能吞掉后面所有合法产物。
🔑 一个容易忽略的一致性要求 这份围栏逻辑必须和浏览器侧的产物解析器保持一致——否则无头运行落盘的和有浏览器时解析的结果会不一样。

落盘时同时写「产物清单」

不要只写文件。每个产物按扩展名生成一份旁挂清单,声明 kind / renderer / exports / entry这份清单让下游知道用哪个渲染器、能导出成什么、哪个是入口文件。没有它,你的文件工作区只能靠扩展名猜。

✅ 本步验收 给解析器喂一段包含「代码围栏里的假 artifact + 真 artifact + 一个未闭合的坏 artifact」的 stdout,只应该落盘那一个真的
06

接住产物:两档执行画像

不同 CLI 的能力差得很远:Claude Code 有完整的 Read/Write/Edit/Bash;DeepSeek TUI 在你选的调用模式下只会吐文本。你不能为每条适配器写一套交付逻辑。抽象成两档——一个 249 字节的文件:

export type ExecutionProfile = 'filesystem' | 'text_artifact';
export function executionProfileFromStreamFormat(streamFormat) {
  return streamFormat === 'plain' ? 'text_artifact' : 'filesystem';
}
flowchart TB
    RUN["一次生成"] --> P{"执行画像"}
    P -->|filesystem| F1["CLI 用原生工具直接写项目文件"]
    F1 --> F2["文件事件 → 文件工作区 → 预览"]
    F2 --> F3["助手以普通摘要收尾
❌ 禁止再输出 artifact 源码块"] P -->|text_artifact| T1["模型循环里没有任何文件工具"] T1 --> T2["唯一交付形态:一个完整 artifact 块"] T2 --> T3["run 结束后宿主扫 stdout 提取并落盘"] T3 --> F2
图 4 两档的提示词契约是相反的:filesystem 档明令禁止输出源码块,text_artifact 档则说源码块是唯一交付形态。
一个真实 bug(issue #313):不加顶部覆盖会怎样 「…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.」

模型不会说「我没有这个工具」,它会假装调用。UI 什么也渲染不出来,用户看到一堆乱码。

修法:把 API 模式覆盖钉在绝对顶部——比发现层还靠前。因为发现层自己开头就写着「以下规则覆盖后文一切」,你必须压在它上面。

一个值得学的架构回归

早期版本在 daemon 里实现过一个「直连 Anthropic + 自己实现 Read/Write/Edit」的兜底循环。现在被彻底删掉了,换成把 BYOK 凭证翻译成 OpenCode 配置,让装好的 opencode 进程继续拥有模型/工具循环

There is no daemon-owned fallback loop and no daemon implementation of Read/Write/Edit tools.

为什么值得学:那个兜底循环违背了「我们不实现 Agent 循环」这条根本主张。一旦你开了这个口子,它会慢慢长成第二个(更差的)Agent。删掉它是对的。

✅ 本步验收 用一条 plain 适配器跑同一个简报,产物文件应该和 filesystem 档跑出来的落在同一个位置、有同样的清单
Part C

给它剧本

从这里开始,我们离开「怎么调 CLI」,进入真正的产品

07

给它品牌:DESIGN.md 契约与 token 注入顺序

品牌不是一堆 CSS 变量,是一份可版本化的契约

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

关键约束:manifest.id 必须等于文件夹 slug 且用规范化 ASCII;文件名固定;每条声明的路径必须安全、相对、存在——这是 guard 检查的。

富文件是缓存,不是竞争的真理源

文件性质
DESIGN.md · tokens.css真理源
components.manifest.jsoncomponents.html + tokens.css 派生
design-tokens.json由 token 契约报告派生,必须与 tokens.css 一致
tailwind-v4.csstokens.css 派生

派生文件一致性由 guard 校验。这条规则防的是「有人改了 tokens.css 但忘了重生成,模型读到两套冲突的值」。

flowchart TB
    L1["① 包专属 USAGE.md(或默认使用契约)"] --> L2["② 完整的 DESIGN.md 正文"]
    L2 --> L3["③ import-mode 指引(声明了才有)"]
    L3 --> L4["④ tokens.css"]
    L4 --> L5["⑤ 紧凑组件清单
(无清单时用 components.html)"] L5 --> L6["⑥ 富文件按需拉取索引"] L6 --> L7["⑦ craft 工艺规则"] L7 --> L8["⑧ 活跃技能/模板正文"]
图 5 注入八层。为什么 USAGE 在最前:它是「读法说明」,告诉模型下面这堆东西怎么用。为什么 craft 在设计系统之后、技能之前:优先级是「品牌 token 赢冲突 > craft 规则补空白 > 技能定义工作流」。

默认使用契约值得原样抄:

Read DESIGN.md for visual principles, paste tokens.css verbatim into the first <style> when it is provided… Treat any pull-layer index as optional context for deeper inspection; do not assume those files have already been loaded.

最后半句在防一个具体的幻觉:模型看到索引就以为文件已经读过了,然后引用一个它没读过的组件。

三件刻意不做的事

不做
不拷进 run 的 cwd
它是只读参考,不是工作副本;拷进去 agent 就可能改它。
不做
不按段裁剪
裁剪会产生「模型只看到色板没看到用色规则」这类断章取义。
不做
不做 {{ }} 变量替换
模板变量会诱导技能作者把设计系统摆在错误的位置。

质量门槛:约束密度,不约束形态

The package-quality guard requires at least seven substantive H2 headings, without prescribing their names, order, or numbering. Use headings that fit the actual system and keep their decisions synchronized with tokens.css.

上游用九段固定模板,OD 演进成「保留充实度门槛,去掉标题名的僵化」。这是内容契约设计的一个好范式。

用户没有品牌怎么办:品牌提取五步

  1. 定位源:有附件就列出来;给了 URL 就 WebFetch <brand>.com/brand/press/about
  2. 下载样式产物:CSS、品牌指南 PDF、截图
  3. 提取真值grep -E '#[0-9a-fA-F]{3,8}' 抓 CSS 里的 hex;截图靠视觉读排版。绝不凭记忆猜颜色
  4. 编码成契约:写 brand-spec.md——六个 OKLch 色 token(--bg --surface --fg --muted --border --accent)+ display/body/mono 字体栈 + 3–5 条观察到的版式姿态(圆角、边框粗细、accent 预算)
  5. 口头复述:一句话说清将用的系统(「深海军蓝产品画布,单一电光青 accent 在 oklch(68% 0.16 220),几何 display + 系统 body」),让用户能廉价纠偏
一条防幻觉硬规则 用户选了「我有品牌规范」但还没给源要源并停下。不许猜品牌域名,不许发明 token。
✅ 本步验收 换一个设计系统,下一次生成的 :root token 应该整体换掉;把 tokens.css 改坏一个值,guard 应该报派生文件不一致
08

给它工艺:第四根轴 craft/

你现在有三根轴:技能(干什么)、模板(做成什么形状)、设计系统(用什么品牌)。做出来的东西还是一眼假。为什么?

因为有一类知识不属于任何一个品牌,也不属于任何一个技能:ALL CAPS 永远需要 ≥0.06em 字距;var(--accent) 每屏最多出现 2 次;#6366f1 永远是 AI 默认色的破绽;display 字体和 body 字体不该是同一个家族。

这些是称职设计师的肌肉记忆。把它们写进 151 份 DESIGN.md?那是 151 份重复,而且改一次要改 151 处。所以拆出第四根轴。

按需订阅,不是全量注入

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

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

laws-of-ux 是一根兄弟轴 其他文件管「怎么渲染」,它管「该组合什么」:定价页(Hick's / 选择过载 / Von Restorff)、仪表盘(帕累托 / 选择性注意 / 工作记忆)、引导(目标梯度 / 蔡格尼克 / 峰终)、模态(费茨 / 泰斯勒)。

两级执法:诚实地标注哪些是真检查

Auto-checked:接进了 linter 的规则(anti-ai-slop.md 的 P0 列表)
vs
Guidance:其余部分——agent 读、评审者用、linter 不查

而且在文档里逐条标注:凡是没接进 linter 的规则后面都跟着「(guidance, not auto-checked)」。

为什么这很重要 如果你写一份「规则手册」但只有 30% 真的被检查,而文档假装 100% 都被检查,那么半年后没人相信这份手册。标注清楚反而让被检查的那 30% 更有权威。晋升路径也是明确的:「unless a specific rule is later promoted into lint-artifact.ts」。

运行时宽容 vs 仓库严格

场景行为
运行时遇到不存在的 craft slug跳过,不报错——「A missing optional paragraph must not make an otherwise usable runtime bundle fail.」
仓库里 checked-in 内容引用不存在的 slugpnpm lint:craft / pnpm guard 失败
故意的前向引用必须登记在 craft/FUTURE_SECTIONS.md 里才算合法

FUTURE_SECTIONS.md 很妙:它让「计划中但还没写的段落」变成可见的、有登记的,而不是靠一个 typo 悄悄漏掉一整段提示词。

✅ 本步验收 在一个技能里写 requires: [typograpy](故意打错)→ lint:craft 应该报错并指出 manifest 路径;同样的错误在运行时应该只是跳过
09

给它剧本:三条硬规则

现在你有了引擎、有了品牌、有了工艺。用户输入「帮我们做个落地页」,模型开始想……然后 25 秒后吐出一整页 HTML,用米色背景、紫色渐变、三个 emoji 图标。用户关掉页面走了。

问题不在模型,在你没给它剧本。

sequenceDiagram
    participant U as 用户
    participant A as Agent
    participant H as 你的宿主
    Note over A: RULE 1 · 第 1 回合
    U->>A: "帮我们做一个 SaaS 落地页,用我们公司的品牌"
    A->>H: 一句短散文 + question-form + 停
    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: 品牌提取五步 A->>H: 写 brand-spec.md A->>U: 一句话复述系统 else 分支 B:没有品牌源 A->>A: 用活跃设计系统 / 自己从方向库挑 Note right of A: ❌ 绝不再弹第二个方向问卷 end Note over A: RULE 3 · 第 3 回合起 A->>H: TodoWrite 九步计划 loop 每完成一步 A->>H: 立刻标 completed end A->>A: 第7步 checklist(P0 全过) A->>A: 第8步 五维自评(<3/5 就返工) A->>H: 第9步 交付
图 6 三条硬规则。整条链路的产品动机只有一句:「问卷就是你的首字节时间」

RULE 1:第一回合只能发问卷

your very first output is one short prose line + a <question-form> block. Nothing else. No file reads. No Bash. No TodoWrite. No native tool calls. No extended thinking. The form is your time-to-first-byte.

用户容忍不了 15 秒的沉默,但完全能接受 2 秒内弹出一张能一路点完的表单。而且必须堵死模型的借口

The form applies even when the user's brief looks complete… 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.

只有三种情况允许跳过:① 用户在已有设计里做微调;② 用户明说「skip questions / just build」;③ 用户消息以 [form answers — …] 开头。

表单编写规则里的六条工程细节

① 硬上限
每张表最多 5 题
「Before emitting, count the questions… A question earns its place only if its answer genuinely changes what you would build for THIS brief.
② 键顺序
default 要写在 options 前面
「the host renders forms token-by-token, and a default that trails a long options array reaches the user late.」——流式渲染导致的 JSON 键顺序要求。
③ i18n
显示层本地化,控制层英文
「write what a native speaker would naturally say, never a word-for-word translation」。但 id/type/value 和分支值必须保持英文——RULE 2 要按 value 匹配。
④ 去重
别自己写「其他」选项
宿主会自动给每个有限选项题渲染本地化的「Other」逃生舱。模型再写一个就重复了。
⑤ 同等权威
元数据 = 插件输入
任一来源提供了答案 → 删掉对应默认问题;标了「(unknown — ask)」→ 新增问题。甚至列出同义映射:platform/surface/target 都答「目标平台」。
⑥ 控件
富控件优先
17 种类型。数值强度用 range、品牌色用 color、要上传就用 file——在同一张表里,不要表单发完再用散文要文件。

RULE 2:四步优先级的分支解析

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

分支 B 明确禁止二次问方向:「Do not emit any second direction-picking formpick the best-matching direction yourself and bind it without asking.」这是一次产品决策的沉淀:早期版本会弹「五选一方向卡」,后来发现多一次点击就多一次流失

RULE 3:九步计划 + 两道非协商闸门

1.  读活跃 DESIGN.md + 技能资源(template.html, layouts.md, checklist.md)
2.  绑定 token 到 :root
3.  规划章节/幻灯/屏幕清单(写之前先口头说一遍4.  把种子模板拷到项目根
5.  粘贴并填充规划好的版式
6.  用简报里的真实、具体文案替换 [REPLACE] 占位
7.  自检:跑 references/checklist.md(P0 必须全过8.  评审:五维雷达(任一 <3/5 就修9.  交付
自评维度拷问
哲学视觉姿态和要求的匹配吗?还是漂回了你最爱的默认?
层次每屏眼睛有一个明显落点吗?还是所有元素在互相竞争?
执行排版、间距、对齐、对比——是对的,还是只是「差不多」?
具体性每个词、数字、图片都是这个简报专属的吗?
克制一个 accent 最多两次、一个决定性亮点——还是三个亮点在打架?
「Two passes is normal.」 默认就该返工两轮。把「一次成型」这个不现实的期待从流程里拿掉,模型就不会为了一次交付而降低自评标准。

Deck 的「框架优先」铁律

Decks especially — framework first, content second. … 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.

实现上有三个分支(每个都是一次事故的化石):deck 项目注入通用骨架;自由形态项目注入带条件前缀的骨架有技能种子时不注入(种子自己有更有主张的框架,重复会冲突)。

第二个分支的条件前缀值得抄:「If — and only if — the brief reads as slides, keynote, presentation, deck, PPT, or 讲解, follow the framework below. Otherwise ignore everything in this section.」——给一段「可能不适用」的指令加上显式的适用条件,模型就不会硬套。

✅ 本步验收 新开一个项目发一句模糊简报 → 2 秒内出现问卷卡,且每题都有预填值;直接提交不改 → 应该能跑出一个合理的产物。
10

管住优先级:提示词是一场「谁压谁」的博弈

到这一步,你的系统提示词已经有 20 层了。它们会互相打架。举三个真实的冲突:

冲突谁该赢怎么保证
发现层说「turn 1 必须问方向」vs 已选了设计系统设计系统最尾部加设计系统方向覆盖
发现层说「调用 TodoWrite」vs API 模式没有工具API 模式把 API 模式覆盖钉在绝对顶部
记忆说「用户喜欢深色」vs 本次品牌是浅色品牌在记忆块前言里显式写「brand wins on conflict」

三种压制手段

手段一
位置
大多数模型对提示词的开头结尾更敏感。

钉在最顶:适用于「这一整段后文都要被推翻」的覆盖。
钉在最尾:适用于「要压过前面某个具体规则」的覆盖。
手段二
显式仲裁语句
不要指望模型自己推断谁赢。写出来。

「when they collide with the active design system tokens, the brand wins; when they collide with the active skill's workflow, the skill wins
手段三
条件门控
与其写一段「如果 X 就忽略下面」,不如在组装时就不放进去

Ask 模式是最好的例子:直接不组装发现层(~3000 token)、方向库、宪章、deck 框架、媒体契约……但保留记忆、指令、设计系统、技能。

Ask mode is light, not amnesiac.

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

一个正在进行的重构:slim vs classic

两套装配路径并存:classic 分层堆叠(现役),slim 把三块塌缩成一份宪章文档(A/B 验证中)。注释写得很坦白:「the classic stack keeps the legacy layered composition until the A/B comparison signs off」。

这个做法值得学 提示词重构的风险极高(改一个词可能让通过率掉 20%),所以不要一次性替换,两套并存 + A/B。
✅ 本步验收 选一个设计系统后开新项目 → 问卷里不应该出现方向/主题色问题;切到 Ask 模式 → 系统提示词长度应该掉一个数量级
想手动拨条件看层怎么装配?去解析站的 LAB 02 提示词分层装配器
11

让它便宜:按变化频率分带 + 缓存命中归因

你的系统提示词 30–80 KB。用户在一个项目里聊 20 轮。如果每轮都是全新的前缀,你烧掉的钱可以按数量级优化。LLM 的前缀缓存是前缀匹配的:只要前 N 个 token 一样就能命中。所以排序决定成本。

flowchart LR
    Z1["① 全局静态
设计师宪章 · 注入抵抗
【所有会话共享】"] --> Z2["② 会话稳定
模式覆盖 · locale
【一个会话内不变】"] Z2 --> Z3["③ 项目稳定
设计系统 · 技能 · 元数据
【一个项目内不变】"] Z3 --> Z4["④ 回合可变
deck/media/platform 信号触发块
【每回合可能翻转】"]
图 7 四带排序。把最稳定的放最前,一个回合中途翻转的信号只会作废缓存后缀,而不是整份提示词。

最精妙的一条:触发信号的稳定性决定块的位置

同一个内容块,根据它是被什么信号触发的,放在不同的带

// 元数据信号(项目创建时固定)→ 放在项目稳定带
if (isSlimCore && metadataPlatformSignal) {
  parts.push(PLATFORM_CONTRACTS_BLOCK, '\n\n---\n\n');
}
// 对话文本信号(中途可能翻转)→ 推到回合可变后缀
else if (isSlimCore && (platformHintSignal ?? false)) {
  slimTurnVariableParts.push(`\n\n---\n\n${PLATFORM_CONTRACTS_BLOCK}`);
}

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… an early insert would break the cached prefix for every section after this line.

同一个块,放错位置就毁掉后面所有段的缓存。

缓存命中归因:出问题时你要知道是哪一段

if (!isResuming)                 → missReason: 'new-session'
if (storedHash === currentHash)  → hit: true
if (storedHash === null)        → missReason: 'missing-stored-hash'
else                             → missReason: 'stable-prompt-changed'
                                   + changedSections: 逐段 diff
一个做遥测时要小心的细节 changedSections 只在真正漂移时计算missing-stored-hash没有基线的老行,在那里报告「所有段都变了」会淹没真正关心的信号

做遥测时要小心这类「技术上正确但信息量为零」的输出。
✅ 本步验收 同一项目连聊 5 轮,第 2 轮起 hit: true;中途切换设计系统 → missReason: 'stable-prompt-changed'changedSections 精确指出是设计系统那一段。
Part D

装闸门

你控制不了推理,但你可以控制什么样的产出算合格

12

第一道闸门:把「一眼假」写成正则

「这个页面一眼是 AI 做的」——听起来完全主观。但拆开看,其中相当大一部分是可枚举的具体模式:

模式具体到什么程度
默认 Tailwind indigo 当 accent精确到 7 个 hex#6366f1 #4f46e5 #4338ca #3730a3 #8b5cf6 #7c3aed #a855f7
两段式「信任渐变」蓝色系 13 个 hex × 青色系 8 个 hex 的配对
emoji 当功能图标17 个:✨ 🚀 🎯 ⚡ 🔥 💡 📈 🎨 🛡️ 🌟 💪 🎉 👋 🙌 ✅ ⭐ 🏆
display 用无衬线h1/h2/h3 的 font-family 落在 Inter / Roboto / Arial / -apple-system / system-ui / SF Pro
发明的指标10× faster · 99.9% uptime · zero-downtime · 3× more productive
填充文案lorem ipsum · feature one|two|three · placeholder text · sample content
能枚举 → 能写正则 → 能自动检查 这是整个 Open Design 里最有创造性的一个主张。别的项目做「工具调用是否正确」的校验,你要做的是「这东西看起来像不像 AI 拉的」的校验

真正的工程含量在哪:ALL CAPS 字距那一条

// ❌ 天真实现,会漏一半
const m = /letter-spacing:\s*([\d.]+)em/.exec(body);
if (!m || parseFloat(m[1]) < 0.06) report();
flowchart TB
    S1["① extractCssTokens(html)
收集每个作用域的 --name: value"] --> S2 S2["② buildResolvedThemes(scopes)
把全局主题作用域组合成多套主题"] --> S3 S3["③ resolveCssVars(body, tokens)
递归解析 var(--x),最大深度 4"] --> S4 S4["④ resolveFontSizePx(decls)
同规则里的 font-size 折算成 px(root=16)"] --> J J["判定:letter-spacing 在每套主题下、
按该字号是否 ≥0.06em 等效"]
图 8 真实实现要做四件事。为什么要折算字号letter-spacing: 1px 在 12px 字上够(0.083em),在 48px 字上远远不够(0.021em)。为什么要解析变量letter-spacing: var(--caps-tracking) 不该被当成「没设置」。为什么要多套主题:亮色下 token 是 0.08em,暗色下可能被覆盖成 0.02em。
一个字符类差别导致的漏检 「The body alternation is [^{}]* (not [^}]*) so the regex matches only innermost rules. With [^}]*, an outer @media (...) { .display { font-size: 48px; text-transform: uppercase; … } } matches as a single rule whose selector is the @media wrapper… the same-rule font-size is lost, and the check falls back to the lenient path that accepts 1px tracking on a 48px heading.

[^}]*[^{}]*一个字符的差别,决定 @media 里的大写标题会不会被漏检。

三层假阳性防护 + 有理由的阈值

假阳性比漏检更致命——报错报烦了没人看。三道防护:剥 HTML 注释(注释里常有教学示例)、剥 CSS 注释/* ... */ 浏览器不渲染但正则会匹配)、区分 token 定义 vs 直接使用(把 indigo 定义成 token 是合法的)。

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.

每个阈值都应该能说出它是从哪个真实场景倒推的,否则半年后没人敢动它。

反馈回路:给 agent 的报错必须自带修复动作

<artifact-lint>
2 P0(必须修), 1 P1(应该修), 0 P2(可以修)。
下一回合重发修正版 <artifact> —— 不要另写解释,用户手上已经有上一版了。

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

三个要点:按严重度排序(P0 在前)、明确要求重发修正版(不要写解释)、每条都带 fix——只给 message 模型会去猜,猜错就多一轮。

一个诚实的边界:不硬阻断 P0 命中不阻止落盘。硬阻断会让「模型死循环修不好」变成「用户什么都拿不到」。闸门的作用是推动改进,不是阻止交付。
✅ 本步验收 手写一份带紫色渐变 + 🚀 图标 + lorem ipsum 的 HTML 喂进 linter → 应该报 3 条 P0;给它一份用 letter-spacing: var(--tracking)--tracking: 0.08em 的大写标题 → 不应该误报。
想直接改 HTML 看实时结果?去解析站的 LAB 03 反 AI 味 linter 实验台
13

第二道闸门:五位陪审员的评审剧场

正则能抓「用了 indigo」,抓不到「这个层次结构很混乱」。第二道闸门用模型评模型。

角色评什么权重
Designer版式、构图、层次0.0
Critic是否真的满足简报;对比度、字重、可读性0.4
Brandtoken 合规、语气、品牌色使用0.2
AccessibilityWCAG、焦点环、语义结构、alt 文本0.2
Copy语气、简洁度、错误文案质量0.2
Designer 权重为 0 是刻意的 「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.」

保留席位、权重归零——定性意见进记录,但不让主观审美卡住发布。

最关键的实现决定:一个会话,不是五个进程

❌ 直觉做法:为每位陪审员开一个进程/会话 → 五份互不知晓的上下文 → 评审结果自相矛盾;而且鉴权/环境变量/日志要维护五份
✅ 正确做法:五位陪审员 = 同一 CLI 会话的五个回合,用 <PANELIST role="…"> 标签分隔,解析成 panelist_* 事件。运行契约和普通生成完全一致:same auth, same env, same logs

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.

收敛循环与五种结算状态

composite = designer×0.0 + critic×0.4 + brand×0.2 + a11y×0.2 + copy×0.2
threshold = 8.0 / 10       maxRounds = 3
perRoundTimeoutMs = 90_000   totalTimeoutMs = 240_000
fallbackPolicy = 'ship_best'   // 或 'ship_last' / 'fail'

五种结算状态:Shipped / Below threshold / Timed out / Interrupted / Degraded

Interrupted 的文案要单独写 「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.

不要让「被中断」看起来像「发货了」。

跨 25 条 CLI 的一致性纪律

你的评审协议要求 agent 吐 <CRITIQUE> 块。25 条 CLI 的模型不一样,遵守程度也不一样。所以要有降级机制(5 种原因,含 adapter_unsupported 的 24h TTL)和一致性门槛

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 degraded for 24h.

…globally after ≥ 90% of production adapters maintain conformance for 14 consecutive days.

这是把「依赖不可控的第三方」工程化的正确姿势 用「连续 14 天 ≥90% 一致性」作为全量开关,而不是靠拍脑袋决定哪天上线。

还要可回放:每次 run 写一份结构化 .ndjson。评审是个多轮过程,用户看完了想再看一遍「它到底为什么给我 6.2 分」——没有回放,这个信息就丢了

✅ 本步验收 故意给一份低质量产物 → 应该跑满 3 轮且状态是 Below threshold;中途按 Esc → 徽章文案应该是 Interrupted at round N, best composite X.X
想拖动分数看合成分怎么变?去解析站的 LAB 04 五陪审评分器
14

让它记住你:三张卡的记忆双环

记忆的三条铁律

铁律一
记忆是偏好,不是硬规则
前言必须显式仲裁:「the brand wins」「the skill wins」。不这么写,模型会把「用户上次说喜欢深色」压过「本次品牌是浅色」。
铁律二
改变的是「你知道什么」,不是「你跳过什么」
「changes only WHAT you know going in; it never shortcuts the standard build flow」——你仍然要 TodoWrite、仍然要跑反 AI 味自检。
铁律三
不许声称记住了,除非同一条回复里有那张卡
这是全步最重要的一条,见下。
flowchart TB
    IN["用户短请求"] --> MEM{"记忆够不够
扩写成简报?"} MEM -->|够 & rewrite=ON| C1["① task-brief 卡
替代 turn-1 问卷"] MEM -->|不够| FORM["走 RULE 1 问卷"] C1 --> BUILD["TodoWrite + 构建
【不可跳过】"] FORM --> BUILD BUILD --> SLOP["反 AI 味 / 品牌自检
【不可跳过】"] SLOP -->|verify=ON & 有已验证规则| C2["② verify-scorecard 卡
宿主程序化检查其存在"] SLOP --> HAND["交付收尾"] C2 --> HAND HAND --> FB{"用户纠正里
隐含可复用规则?"} FB -->|是| C3["③ rule-proposal 卡
Keep / Edit / Discard"] C3 -->|点 Keep| STORE[("已验证规则库")] STORE -.->|下次注入| MEM
图 9 记忆双环。中间那条「构建 + 自检不可跳过」是硬约束——「Skipping the discovery form when intent is already understood is correct; skipping TodoWrite or the anti-slop gate is not.
① PRE 环
task-brief
短请求被记忆扩写成完整简报时,在回复最开头发一张折叠卡。

约束:每回合最多一张;请求已明确或很琐碎就跳过;绝不以散文形式输出简报,只能是卡片
② POST 环
verify-scorecard
这里出现程序化执法:「The daemon programmatically checks this scorecard — a missing scorecard… is recorded as an enforcement failure.」

收尾顺序被规定死:(1) 自检并就地修 → (2) 发记分卡 → (3) 正常交付。「Prefer fixing silently over asking.
③ 写回环
rule-proposal
提案而非静默保存。每回合最多一条,且只在确信它能泛化时提。

「the rule becomes saved only after the user clicks Keep.」
最后半句在治一个具体的模型撒谎行为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.

模型很爱说「好的,我记住了」,但实际上什么也没记。解法是把「记住了」这个断言绑定到卡片的存在性上——没卡就不许说。
为什么这套设计值得抄 对比:openworker 是显式 SQLite 事实(设置面板可见)、Raven 是 EverOS 双轨(部分可见)、nanobot 是 Dream 夜间反思(过程不可见)。

核心差别:这套设计把「模型的内部状态」变成了「用户能看见、能改、能拒绝的 UI」。写入需要用户点确认;检查有程序化执法;每一条规则都能追溯到「是哪次纠正产生的」。
✅ 本步验收 连续三次纠正同一件事 → 第三次应该出现 rule-proposal 卡;点 Keep 后下一次生成应该出现 verify-scorecard 且包含这条规则。
15

让它可扩展:四平面 + 原子 + 封闭 until

my-plugin/
├── open-design.json    ← 必需:市场元数据 + inputs + pipeline + capabilities
├── SKILL.md            ← agent-skill / scenario 类型必需
├── README.md           ← 可选
├── preview/            ← 可选:index.html / poster.png(视觉类强烈建议)
└── examples/           ← 可选

一条重要的默认od.capabilities[]声明最小集——受限安装默认只给 prompt:inject

原子:宿主暴露给插件的能力单元

Plugins never own the atom implementations; they only reference them by id. 宿主负责把每个原子解析成:系统提示词片段 + 工具门控 + GenUI 面声明。

flowchart TB
    M["插件 manifest
od.pipeline.stages[*].atoms[]"] -->|①解析| PS["PipelineStage[]"] PS -->|②run 前| B["解析内置原子指令体
渲染成 ## Active stage 提示词块"] B --> R["③运行时逐阶段走"] R --> E1["发 pipeline_stage_started"] E1 --> W["④向 worker 注册表要
宿主可观测的信号"] W --> A["⑤往 run_devloop_iterations 写一行审计"] A --> E2["发 pipeline_stage_completed + 信号"] E2 --> R
图 10 流水线执行。一处必须诚实的说明:发生在 CLI 内部的原子(如 file-write)只能给宽松信号,因为「the daemon has no independent observation for that tool action」——不要假装你能观测一切

until 词汇表必须是封闭的

信号谁发出
critique.scorecritique-theater
iterations内建计数器
user.confirmedconfirmation GenUI 面解析时
preview.oklive-artifact 预览流水线
build.passing / tests.passingbuild-test 流程
任何面向第三方开放的扩展点,都要问一遍:我在这里放的是数据还是代码? 「The evaluator is deliberately closed and is not arbitrary JavaScript. Unknown signals fail parsing and od plugin doctor reports them.」

如果插件能写任意 JS 作为收敛条件,你就得沙箱它、审计它、担心无限循环、担心它读环境变量。封闭词汇表把「插件能表达什么」限制在宿主能保证的语义内——代价是表达力受限,换来的是插件市场可以开放安装。

原子的晋升路径

不要一上来就把新能力做成内置原子:① 先作为树外插件实现 → ② 形状稳定后加内置原子、往目录追加一行、有真实可观测信号时才注册 worker → ③ 同一个 PR 更新文档和 spec 表格 → ④ 通过 pipeline 引用 / API / CLI / doctor 触达。

✅ 本步验收 写一个插件,until: "critique.score >= 8" 能跑;改成 until: "myCustomThing == true"od plugin doctor 应该报未知信号
Part E

装成产品

边界、桌面壳、MCP 服务端、导出。最后回放整条链路。

16

装边界:你放弃了权限闸门,就必须补齐外围

先诚实面对:你的 buildArgs 里有这些 --permission-mode bypassPermissions(Claude)· --force(Cursor)· --permission-mode dangerous --respect-workspace-trust false(Devin)· --yolo(Qoder / Trae)· --allow-all-tools(Copilot)· --auto(DeepSeek)· --dangerously-allow-all(Amp)

为什么必须这样?因为你在没有 TTY 的环境里跑它们。交互式批准提示会直接把 run 挂死——用户在浏览器里等着,子进程在等一个永远不会来的 y

所以要把这件事写进文档,不要藏:「users must treat these runs as trusted agent execution」。

放弃了执行层的闸门,就必须把预算全花在边界上。六条:

边界一:默认 loopback

daemon 默认绑 127.0.0.1;LAN 暴露需要同时设两个环境变量(缺一不可);连接器凭证和预览路由无论如何都保持 loopback-only——即使公开部署也不放开。

边界二:SSRF——默认封内网,opt-out 极其严格

你有一个 BYOK 代理,用户可以填任意 baseUrl这是教科书级的 SSRF 面。默认封锁解析到私有/内部地址的 URL——RFC1918、link-local、CGNAT、云元数据 IP169.254.169.254 那类,能偷 IAM 凭证)。

但真实用户确实有内网网关(VPN 里的 LiteLLM、Ollama),所以要有 opt-out,且必须严格:严格 opt-in(默认空)· 精确主机匹配(不做子域名/子串匹配)· 范围受限(只作用于你自己配的 provider 端点)· 不放宽下游(上游响应里返回的下载 URL 仍然被封)· 错误项丢弃(畸形条目和 CIDR 记法被丢弃并告警,不静默信任)。

说清残余风险 「Allowlisting a hostname trusts whatever it resolves to; allowlist the resolved IP instead if you want the DNS-resolved address re-checked.」

放行主机名 = 信任 DNS 解析结果(DNS rebinding 风险)。说清楚残余风险比说「我们很安全」有价值得多。

边界三:桌面文件夹导入的 HMAC 单次令牌

用户要导入本机任意文件夹。这个能力很危险——渲染进程里的一段恶意脚本能不能构造一个请求,让 daemon 去读 /etc/

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: 打上服务端控制的「可信选择器」标记 Note over D: 之后每次文件访问都对该外部根做安全路径解析
图 11 桌面信任链。关键一条:那个「可信选择器」标记由服务端控制,普通的项目创建/更新请求伪造不了——渲染进程拿不到 HMAC 密钥。

边界四:预览必须是沙箱 iframe

沙箱 iframe,无宿主同源访问;每个面只 opt-in 它需要的特性;切换渲染模式时两个 frame 都保持挂载避免闪烁;消息处理器校验发送方 iframe;需要来自活跃 frame 的信号会再次核对活跃窗口——最后两条防的是:页面里有多个 iframe 时,一个后台 frame 冒充活跃 frame 发消息

边界五:技能暂存必须是拷贝,不是软链

❌ 错误版本(PR #435 round 1):把 .od-skills/ 做成指向仓库 skills/ 的目录软链。

评审意见:「write-amplification vulnerability: agents have write access to their cwd, and a Write/Edit/Bash call… resolves through the symlink and mutates the shipped resource itself.

一次 Edit 就能改坏所有项目共用的技能源文件。
✅ 正确版本:每个项目一份真实拷贝

别名 = <folder>-<源路径 sha256 前 10 位>,因为用户根和内置根可能有同名技能。

配套:只暂存活跃技能(CoW 让稳态成本只是几个 syscall)· dereference: true(拷贝完全自包含)· stat() 而非 lstat()(跟过内容寻址挂载的软链)· 跨文件系统流式拷贝兜底EXDEV/EPERM)· 提示词里给两条路径(暂存失败仍能工作)。

边界六:Windows 命令行长度的三重守卫

如果你必须支持一条只吃 argv 的 CLI,你需要三道守卫:

守卫时机检查
checkPromptArgvBudgetbin 解析前(快)原始提示词字节数 vs maxPromptArgBytes(如 30 000)
checkWindowsCmdShimCommandLineBudgetbuildArgs.cmd/.bat shim:用平台层相同的引号翻倍规则重算 cmd.exe /d /s /c "…"
checkWindowsDirectExeCommandLineBudgetbuildArgs非 shim 的 .exe:用 libuv quote_cmd_arg 规则(每个 "\",紧邻引号的反斜杠翻倍)重算

两个 Windows 守卫互斥。三者一起抓的是:原始字节数没超,但引号密集的提示词(代码块、JSON 形状的技能种子)展开后超过 CreateProcess 的 32 767 字符上限。三者发同一个可行动的错误:告诉用户「减少技能/设计系统上下文、缩短对话、或换一个支持 stdin 的适配器」。并且三者都有单测,「so the guards can't silently regress」。

✅ 本步验收 改坏 .od-skills/xxx/SKILL.md → 源 skills/ 毫发无损;在 BYOK 里填 http://169.254.169.254/被拒并给出明确原因;Windows 上用含大量引号的 40 KB 提示词跑 argv-only 适配器 → spawn 前就给出 AGENT_PROMPT_TOO_LARGE,而不是一个看不懂的 ENAMETOOLONG
17

装成产品:桌面壳 + 侧车 + MCP 服务端 + 导出

三种运行形态,一套代码

形态入口特征
源码开发pnpm tools-dev run web动态分配端口,daemon + web 侧车
打包桌面 / 无头Electron / headless 启动器解析 channel/namespace 作用域的运行时与数据身份后再拉 daemon
容器 / daemon 直服docker compose up -d同一个 daemon 直接服静态导出 + /api/*

Ports are transport details; they do not define process identity, namespaces, or daemon data roots.

打包桌面模式下 Electron 不假设端口——它通过 sidecar IPC 去问 web 的真实 URL。

数据根契约:一处值得抄的文档纪律

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

为什么值得单独立规矩 多处文档各自写死一个路径,是所有本地优先应用的经典腐烂源:改了实现,八个 md 里有六个还写着旧路径,用户按文档找不到数据。

把路径降级成单点权威,其他文档只允许引用不允许复述。实现上:启动时解析一次成 RUNTIME_DATA_DIR,之后所有 daemon 拥有的数据全部由这个根派生。唯一例外是文件夹导入。

dual-track 规则:能力必须同时在 UI 和 CLI 出现

When adding a user-facing capability, close the loop in one change: contract type, daemon route, web surface if applicable, and CLI command with --json plus --prompt-file <path|-> for long prompts where relevant.

为什么强制?因为 CLI 是外部 agent 消费你的产品的方式。如果一个能力只有 UI 有,那么通过 MCP 接进来的 Claude Code 就用不了它——你的「可被任意 agent 消费」这个卖点就漏了一个洞。而且注意 --json每个命令都要支持,这样才能 | jq | xargs 进自动化。

把自己做成 MCP 服务端

这是「agent 原生」这个定位的闭环:你不仅调用别人的 agent,还要被别人的 agent 调用。

od mcp install <agent>     # 一行装进 16+ 个 CLI 的配置
# 然后在那个 agent 里:
od project list --json
od files read <project-id> <relative-path>

Why MCP? 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.

一个真实的坑:选命令名前先 which 一遍 macOS / WSL2 上 /usr/bin/od系统的八进制转储工具,会在 PATH 上盖过你的 od。三种应对:设置面板给一段用绝对路径的片段;install.sh 存在的理由之一就是「fails fast if your shell resolves a non-Open-Design od binary」;文档里三处提醒。

不做跨 agent 自动兜底

A crash, auth failure, timeout, or invalid invocation remains a failure for that run… the daemon does not silently move the request to another detected CLI.

为什么反直觉但正确:自动切换 agent 会让计费、鉴权、输出风格全部悄悄改变,用户根本不知道刚才那份产物是谁做的。

导出矩阵与 mock agent

导出
六种格式
HTML(单文件内联)· PDF(浏览器打印,deck 感知)· PPTX(agent 驱动的技能,不是库)· ZIP · Markdown · MP4(HTML+CSS+GSAP → headless Chrome + FFmpeg)。

PPTX 走技能是因为 HTML→PPTX 的保真度是判断问题,不是转换问题。
测试
mock agent
维护 25 条适配器,每次改解析器都真跑 25 个 CLI 是不可能的。所以要有假 CLI + 录下来的真实流 + 期望输出。

守则:「replay a mock CLI trace instead of burning provider budget」。
✅ 本步验收 od project list --json | jq 能跑;把 daemon 停掉,桌面 App 应该给出明确错误而不是白屏;改一个解析器 → 用 mock 回放能在 5 秒内跑完回归。
🎬 完整回放

这一句话到底跑了什么

用户在 Home 打字:「帮我们做一个 SaaS 产品落地页,用我们公司的品牌。」然后选了设计系统 linear-app,点 Run。

sequenceDiagram
    autonumber
    participant U as 用户
    participant W as Web
    participant D as 宿主 Daemon
    participant C as claude 子进程
    participant F as 项目文件
    U->>W: 输入简报 + 选 linear-app + Run
    W->>D: POST /api/projects
    D->>D: 分配托管项目工作区(RUNTIME_DATA_DIR 派生)
    W->>D: POST /api/chat(SSE)
    Note over D: 【组装阶段】
    D->>D: 探测 agent(已 warm)→ claude 可用
    D->>D: 暂存活跃技能到 .od-skills/saas-landing-3f2a91b0c4/
    D->>D: 组装系统提示词(20+ 层,按变化频率分带)
    D->>D: 算 stablePromptHash → 新会话,miss
    Note over D: 【启动阶段】
    D->>D: resolveAgentLaunch → 拿到真实可执行路径
    D->>D: buildArgs → -p --input-format stream-json …
    D->>C: spawn(cwd=项目工作区),提示词经 stdin,stdin 保持打开
    Note over C: 【第 1 回合 · RULE 1】
    C-->>D: "明白了 — SaaS 落地页,用 Linear 的设计语言。补几个信息:"
    C-->>D: question-form(4 题,全部预填)
    D-->>W: SSE 事件流
    W->>U: 渲染问卷卡(没有方向/主题色题)
    U->>W: 直接提交(不改)
    W->>D: POST /api/chat("[form answers — discovery] …")
    D->>C: 同一 stdin 写入新的 user 消息(不重启进程)
    Note over C: 【第 2 回合 · RULE 2 分支 B】
    C-->>D: 有活跃设计系统 → 不再问方向,直接进 RULE 3
    Note over C: 【第 3 回合 · RULE 3】
    C-->>D: tool-call TodoWrite(9 步计划)
    C->>F: Read .od-skills/… 的 template.html / layouts.md / checklist.md
    C->>F: Write saas-landing.html(绑 linear-app 的 :root token)
    D-->>W: file-write 事件 → 文件工作区 → 沙箱 iframe 预览
    Note over D: 【闸门一】
    D->>D: lintArtifact() → 1 条 P1: accent-overuse(9 次)
    D->>C: artifact-lint 系统提醒(含 fix + snippet)
    C->>F: Edit(把 7 处降级成 var(--fg))
    Note over C: 【自检】
    C-->>D: step7 checklist P0 全过
    C-->>D: step8 五维自评:具体性 2/5(填充文案)→ 返工
    C->>F: Edit(替换 [REPLACE] 为真实文案)→ 重评全部 ≥3/5
    Note over D: 【闸门二 · 若开启】
    D->>C: Design Jury round 1(同一会话的 5 个回合)
    D->>D: composite = 8.3 ≥ 8.0 → Shipped at round 1
    Note over C: 【交付 · filesystem 档】
    C-->>D: 普通摘要(❌ 不发 artifact 源码块)
    D-->>W: done
    W->>U: 预览 + 导出按钮
    U->>W: 导出 PDF
    
图 12 完整链路。注意第 1 回合到第 2 回合没有重启进程——同一个 stdin 写入新的 user 消息,子进程保住了它读过的文件和工具历史。

这条链路上,你写的代码在哪?

环节谁的代码
探测、启动、argv 构建你的
提示词组装(20+ 层)你的(且这是你最重要的产品)
技能暂存你的
理解简报、决定问什么、写 HTML、修改claude 的
流解析 → 统一事件你的
反 AI 味 linter你的
评审剧场编排你的(评分是 claude 做的)
预览、导出你的
一句话 推理全是别人的,产品全是你的。
附录 A

验收断言(做完每步怎么验)

点一下打勾,进度存在浏览器本地。全部通过 = 你有一个能跑的设计 Agent 宿主。

01能用一句话说清「为什么不写主循环」,并列出这个决定带来的三个新问题
02新增一条已知 wire format 的 CLI = 一个新文件 + registry 一行,引擎零改动
02用户 profile 撞了内置 id → daemon 起不来(加载期 throw),不是安静覆盖
03claude 移出 PATH → 只有它变灰,其他 25 条不受影响
03nvm 装的 shim 版 CLI:探测结果和实际能不能跑一致
0460 KB 提示词在 macOS + Windows 都能跑完
04工具调用中途不断流(tool_use 时不关 stdin)
05喂一段「围栏里的假 artifact + 真 artifact + 未闭合的坏 artifact」→ 只落盘那一个真的
06plain 档和 filesystem 档跑同一简报,产物落在同一位置、有同样的清单
07换设计系统 → 下次生成的 :root 整体换掉
07改坏 tokens.css 一个值 → guard 报派生文件不一致
08技能里写 requires: [typograpy]lint:craft 报错并指出 manifest 路径;运行时只跳过
09新项目发模糊简报 → 2 秒内出问卷卡,每题有预填;直接提交能跑出合理产物
10选了设计系统 → 问卷里不出现方向/主题色题
10切 Ask 模式 → 系统提示词长度掉一个数量级
11同项目连聊 5 轮,第 2 轮起 hit: true
11中途换设计系统 → missReason: 'stable-prompt-changed'changedSections 精确指段
12紫渐变 + 🚀 + lorem ipsum → 报 3 条 P0
12letter-spacing: var(--tracking)--tracking: 0.08em不误报
13低质量产物 → 跑满 3 轮且状态 Below threshold
13中途 Esc → 徽章是 Interrupted at round N…不是 Shipped
14连续三次纠正同一件事 → 出现 rule-proposal
14点 Keep 后下次生成出现 verify-scorecard 且含这条规则
15until: "critique.score >= 8" 能跑;未知信号 → od plugin doctor 报错
16改坏 .od-skills/xxx/SKILL.md → 源 skills/ 毫发无损
16BYOK 填 http://169.254.169.254/ → 拒绝并说明原因
16Windows + 引号密集的 40 KB 提示词 + argv-only 适配器 → spawn 前就报 AGENT_PROMPT_TOO_LARGE
17od project list --json | jq 能跑
17daemon 停掉时桌面 App 给出明确错误而不是白屏
17改一个解析器 → mock 回放 5 秒内跑完回归
附录 B

十二个最容易翻的车

点开看症状、根因、修法。每一条都是真实事故的化石。

症状
UI 说「没装」,但手动跑得好好的(尤其 nvm/fnm/mise + GUI 启动的 App)。
根因
GUI 启动的 App 拿到的 PATH 被系统精简过;shim 在那个 PATH 下跑不起来,但启动解析器能升级到原生二进制。
修法
探测入口第一件事就是调用和 spawn 完全相同的路径解析函数。
症状
Linux spawn E2BIG、Windows spawn ENAMETOOLONG,而且是间歇性的(提示词长度取决于选了哪个设计系统)。
修法
能走 stdin 就走 stdin;必须走 argv 的上三重守卫(含 Windows 引号展开重算)。
症状
回合在工具调用中途莫名结束。
修法
只在干净的 turn_end / usage 之后关。
症状
第 2 回合又弹一次发现问卷,看起来像循环卡死。
根因
CLI 自己有会话记忆,你又把渲染的 transcript 拼进用户消息,它看到自己上回合发的 <question-form> 原文就模式匹配复读。
修法
为这类适配器加 opt-out,跳过 transcript 注入。
症状
项目里多出一堆奇怪的 .html
修法
先算 Markdown 围栏 + 行内反引号的跳过区间;并且和浏览器侧解析器保持一致
症状
API 模式下模型吐 <todo-list> 伪标记;或选了设计系统还在问主题色。
修法
搞清楚每个覆盖要压的是哪一段——要压全局的钉最顶,要压具体规则的钉最尾。
症状
缓存命中率忽高忽低,账单不稳。
根因
一个由对话文本触发的块被插在了项目稳定带,用户中途说一句话就把后面所有段的缓存作废了。
修法
触发信号的稳定性决定块的位置,不是内容的重要性。
症状
agent 每轮都在修 linter 报的问题,但用户看不出区别。
修法
剥 HTML 注释、剥 CSS 注释、区分 token 定义 vs 直接使用;每个阈值都要能说出它是从哪个真实场景倒推的
症状
评审结果自相矛盾;鉴权/环境/日志要维护五份。
修法
五位陪审员 = 同一会话的五个回合,用标签分隔。
症状
一个项目里的 agent 改坏了 SKILL.md所有项目一起坏
根因
agent 对 cwd 有写权限,Edit 会穿过软链改到源文件。
修法
per-project 真实拷贝 + dereference: true + 路径哈希后缀 + 跨文件系统流式兜底。
症状
市场一开放就出现无限循环、读环境变量、超时不退的插件。
修法
until封闭词汇表,未知信号解析失败并由 doctor 报出。
症状
模型说「我记住了」但什么也没记;或者记了一堆用户根本不同意的「规则」。
修法
三张卡——提案卡要用户点 Keep 才落库;并且禁止模型在没有卡的情况下声称记住了
附录 C

源码对照索引

教程步骤Open Design 源码
第 1 步 立论docs/agent-adapters.md:5-11
第 2 步 契约apps/daemon/src/runtimes/types.ts:101-253
第 2 步 样本定义apps/daemon/src/runtimes/defs/claude.ts(98 行)
第 2 步 注册表 + 不变式apps/daemon/src/runtimes/registry.ts:30-76
第 2 步 三种会话续跑apps/daemon/src/runtimes/types.ts:189-209
第 3 步 探测流水线apps/daemon/src/runtimes/detection.ts:238-318
第 3 步 路径解析必须一致detection.ts:243-250(注释)
第 4 步 stdin 与命令行上限defs/claude.ts:45-51(注释)
第 4 步 stdin 生命周期docs/agent-adapters.md:191-194 · apps/daemon/AGENTS.md:113
第 5 步 流格式分组docs/agent-adapters.md:139-147
第 5 步 <artifact> 提取apps/daemon/src/runtimes/plain-stream.ts(473 行)
第 6 步 执行画像packages/contracts/src/execution-profile.ts
第 6 步 两档交付契约daemon/prompts/system.ts:549-572 · contracts/prompts/system.ts:498-540
第 7 步 设计系统包契约design-systems/README.md
第 7 步 注入八层顺序docs/skills-protocol.md:200-218
第 7 步 品牌提取五步contracts/src/prompts/discovery.ts:176-187
第 8 步 craft 四轴 + 两级执法craft/README.md · craft/anti-ai-slop.md
第 9 步 三条硬规则contracts/src/prompts/discovery.ts:25-354
第 9 步 表单编写规则discovery.ts:135-152
第 9 步 deck 框架优先contracts/prompts/system.ts:450-485
第 10 步 组装器apps/daemon/src/prompts/system.ts:791-1370
第 10 步 优先级注释contracts/prompts/system.ts:498-512
第 11 步 缓存分带daemon/prompts/system.ts:851-867(注释)
第 11 步 命中归因runtimes/chat-prompt-inputs.ts:394-442
第 12 步 反 AI 味 linterapps/daemon/src/lint-artifact.ts(1 000 行)
第 12 步 字距 token 求值lint-artifact.ts:636-720 · 805-892
第 13 步 评审配置与文档packages/contracts/src/critique.ts · docs/critique-theater.md
第 14 步 记忆与三张卡contracts/prompts/system.ts:376-404
第 15 步 原子与流水线docs/atoms.md · plugins/atoms.ts · pipeline-runner.ts
第 16 步 授权边界docs/agent-adapters.md:442-465
第 16 步 桌面 HMACapps/daemon/src/desktop-auth.ts · docs/architecture.md:228-243
第 16 步 技能暂存尸检apps/daemon/src/cwd-aliases.ts:1-30(注释)
第 16 步 Windows 三重守卫docs/agent-adapters.md:345 · runtimes/prompt-budget.ts
第 17 步 三形态 + 数据根docs/architecture.md:18-53 · :150-162
第 17 步 dual-trackapps/daemon/AGENTS.md:99-106

结语:这条路线适合谁

适合
✅ 适合你,如果
· 你的产品价值在内容和体验,不在推理能力(设计、写作、数据分析、报表)
· 你的用户已经有 CLI Agent(开发者、设计师、技术型 PM)
· 你想把精力花在领域知识的编码上(品牌契约、工艺规则、质量闸门),而不是重造循环
不适合
❌ 不适合你,如果
· 你的用户是完全不懂技术的普通人(他们不会装 CLI,你得全靠 BYOK 兜底,体验差一档)
· 你需要细粒度的权限控制(这条路线里你控制不了子进程的行为,只能控制边界)
· 你的核心竞争力就是推理本身(那你就该自己写循环)
最后一句 Open Design 三个月拿到 82k star,不是因为它的适配器写得好——适配器只有 3 000 行。是因为它把「什么是好设计」这件事,编码成了 151 个品牌包、11 份工艺规则、330 行行为脚本和 1 000 行审美 linter

引擎是借的,产品是自己的。

继续读