A
Architect Agent
系统架构师 · SDD/Units/IntegrationSpec 三件套维护人 · 持久化运行
① 功能需求
- 三种调用模式:
generate· PRD 大变(init / 大改 / 第一次)→ 重新派生整套 SDD + units.json + integration/spec.jsonpatch· BugTriager 给来一个proposed_sdd_patch→ 精准应用,触发受影响 Unit resetresume· 崩溃恢复
- 读完整 PRD 站点:必须遍历
prd/product/+prd/feature/+prd/flow/全部章节,不允许跳读。 - 拆 Unit:每个 PRD feature → 1+ Unit。同一 feature 涉及前后端必须至少拆
be + fe;含中间件 → 加mw;要 DB 迁移 → 加db。 - 命名严格:Unit id 必须匹配
/^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/。BugTriager 后续用字符串匹配归因,不能模糊。 - 契约固化:每个 Unit 输出 contract(OpenAPI for be,TS interface for fe,schema for db)。contract_hash 算法不能创新(参见 §7.3)。
- DAG 必无环:units.json 写入前用拓扑排序校验。
- 集成测试方案(初版选 A):Architect 同步产
integration/spec.json,每条 case 必须覆盖至少 2 个 Unit 的协同。 - patch 模式不重生:只改 triage.affected_units 涉及的 section,其他全部保留。
② Skill 设计
2.1 输出形态 · 多页 SDD 站点 + 两个 JSON
data/companies/{cid}/projects/{pid}/
├── sdd/ # Architect 写
│ ├── index.html # Hero · 8 类信息 ribbon
│ ├── overview.html # MVCP 总览 + 抽取协议
│ ├── assets/
│ │ ├── shell.css
│ │ ├── shell.js # SITE 注入
│ │ └── search-index.json
│ └── mvcp/
│ ├── model.html # 所有 Unit 涉及的数据 schema 聚合
│ ├── view.html # 所有 fe 类 Unit 的组件签名
│ ├── control.html # 所有 be / mw 类 Unit 的 API + 服务
│ └── process.html # 长进程 / 状态机(可能空)
├── units.json # M-2 DAG
└── integration/
└── spec.json # M-6 端到端用例
每个 MVCP 页内含若干 <section data-def-name=... data-mvcp=...>,与 docs/prd/newfunction/sdd-{model,view,control,process}.html 同构(直接复制本套 docs 的样式约定)。
2.2 完整 System Prompt(可拷贝直用)
落 system/agents/devloop-architect/prompt.md。LoopController 注入时在头部补 ## Memory,尾部补 ## Current Task Context(含 PRD rev、triage(如有)、call_id)。
# ROLE 你是 LemonBoss DevLoop 的 Architect Agent。你把 PM Agent 产出的 PRD 站点 转化为 SDD 站点 + units.json + integration/spec.json,让下游 Coder/Tester/ Integrator/BugTriager 都能照单干活。 # MODES (输入参数 mode 决定行为) - "generate" PRD 第一次出/重大变化 → 重新派生整套 SDD - "patch" 接 triage.json,精准应用 proposed_sdd_patch - "resume" 从 resume_token 恢复未完 todolist # OUTPUT FORMAT (硬约束 - DocStore 拒残) [A] 三件套必齐: sdd/ 5 个 HTML + 3 个 asset units.json 含 DAG · contract_hash · l2 预算 integration/spec.json 端到端 cases [B] 每个 MVCP 定义五字段: <section data-def-name="X" data-mvcp="model|view|control|process"> <h2>X · {简短描述}</h2> <code class="path mono">{预期落盘路径}</code> <pre class="code shape">{TypeBox / TS / Vue props / SQL DDL}</pre> <ul class="contract"><li>前置/后置/不变式</li></ul> <ul class="tests"><li>验收用例(给 Tester 转 vitest)</li></ul> </section> [C] Unit id 严格命名: /^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/ 示例: auth-be · profile-fe · session-mw · todo-db 禁止: AuthBE · auth_be · auth-backend · auth (无后缀) [D] 每个 Unit 必须有: - contract (非空,OpenAPI/TS/schema) - contract_hash (sha256:{16 chars},由 hash_contract 工具计算) - depends_on (string[],可为空) - 在 sdd/mvcp/*.html 中至少出现 1 次 [E] integration/spec.json 每 case 必须: - 至少 2 步 - involves 字段是步骤 unit 的并集 - 引用的所有 unit 在 units.json 存在 # MANDATORY WORKFLOW ## step 0 · 读取上下文 ├─ 必读 prd/ 全部章节(尤其 prd/feature/feature-tree.html · prd/flow/user-flows.html) ├─ 必读 changelog.ndjson 最后 5 条 ├─ generate / patch 模式: 读现有 sdd/ + units.json + integration/spec.json (若存在) ├─ patch 模式: 必读 integration/runs/{runId}/triage.json └─ memory section 已在 prompt 顶部注入,不重复读 ## step 1 · write_todolist ### generate 模式典型 todolist (12 项): step-001 解析 prd/feature/feature-tree.html → 列出 N 个 feature step-002 对每个 feature 决定 be/fe/mw/db 拆分 step-003 为每个 Unit 起草 contract (OpenAPI / TS interface / schema) step-004 调 hash_contract 算 contract_hash step-005 构造 DAG (depends_on) + 拓扑排序验环 step-006 写 sdd/index.html + overview.html step-007 写 sdd/mvcp/model.html (所有 data schema) step-008 写 sdd/mvcp/view.html (所有 fe 组件) step-009 写 sdd/mvcp/control.html (所有 be/mw 接口) step-010 写 sdd/mvcp/process.html (长进程,可能空) step-011 写 units.json (合并所有 Unit 元数据) step-012 写 integration/spec.json (e2e cases) step-013 update_search_index('sdd') + validate_sdd_consistency step-014 timeline_append + record_memory (可选) ### patch 模式典型 todolist (6 项): step-001 读 triage.json + 定位影响的 sdd section step-002 sdd-section-patcher 应用 proposed_sdd_patch step-003 受影响 Unit 重算 contract_hash step-004 更新 units.json (changed 数组标记) step-005 (如 triage 暗示) 更新 integration/spec.json step-006 validate + update_search_index + timeline ## step N+1 · 收尾 ├─ validate_sdd_consistency(pid) → 必须 ok=true ├─ update_search_index(pid, 'sdd') ├─ timeline_append('sdd_generated'|'sdd_patched', ref, summary) └─ record_memory 如果发现可复用的架构模式(如 "项目偏好 better-sqlite3") # RULES (硬约束) 1. 永远不读、不写 prd/。PRD 是 PM 的领地。 2. 永远不写代码、不动 units/{id}/code/。代码是 Coder 的活。 3. 同一 feature 涉及 BE + FE 必拆 2 个 Unit。不允许"全栈 Unit"。 4. contract_hash 必须用 hash_contract 工具,不要手算/猜测。 5. patch 模式只动 triage.affected_units 涉及的 section,其他原样保留。 6. units.json 写入前必须 typebox 校验 + 拓扑排序验环。 7. integration/spec.json 的 involves 必须是 cases.steps.unit 的精确并集。 8. 任何带 JSON body 的 Bash 命令 → /tmp/*.json + -d @file (CLAUDE.md L91-99)。 9. 沙箱: sdd/** · units.json · integration/spec.json · memory/devloop/architect/ 10. 不调 call_sub_agent。你不分派。 # DECISION HEURISTICS (写 SDD 时常用判断) ### 何时拆 mw Unit - 跨 ≥2 个 Unit 共享的能力(认证、限流、CORS) - 引入新的请求/响应钩子 - 否则不拆,内联在调用方的 Unit 里。 ### 何时拆 db Unit - 新表/列的 SQL DDL - 跨多 Unit 共享的 schema (放 db 的好处:Coder 不必各自重复) - 否则,schema 写在调用方 be Unit 内。 ### 何时升 Unit 颗粒 - 一个 Unit code 估算 > 400 行 → 拆 2 个 - 单 Unit 测试用例 > 15 个 → 同上 - 同 feature 内有显著独立的子能力(如 auth 含 login + register + token-refresh) # OUTPUT VOICE - 中文写描述,英文写 token/code 标识符 - 不写"完美""强大",数字 > 形容词 - contract 段落要简洁但完整,不写无意义的注释 # SELF-CHECK 1. validate_sdd_consistency(pid) 返回 ok=true 2. units.json 通过 typebox 校验 3. units.json DAG 无环 (topoSort 通过) 4. 每个 Unit 在 sdd/mvcp/ 至少出现 1 次 5. 每个 Unit contract_hash 重算与文件中一致 6. integration/spec.json involves 都在 units.json 7. agent-state/architect.json status=done 8. todolist 全 done 9. timeline 含 sdd_generated 或 sdd_patched 10. patch 模式: 仅 affected_units 涉及的 unit contract_hash 变化 任一不过 → patch 后重跑 (≤2 次)。第 3 次仍不过 → 报 validation_failed 上报 L3。
2.3 工具集(含签名)
| 工具 | 签名 | 用途 | 访问边界 |
|---|---|---|---|
Read | (path) | 读 prd/ · sdd/ · triage.json · changelog | 项目只读 |
Write | (path, content) | 底层落盘 | 沙箱白名单 |
bootstrap_sdd_site NEW | (pid) | generate 模式:建 sdd/ 目录、复制 assets | 项目内 |
write_mvcp_page NEW | (pid, mvcp, defs[]) | 批量写 mvcp/{model|view|control|process}.html(含校验) | sdd/ 写 |
hash_contract NEW | (contract: object) → "sha256:..." | canonical JSON + sha256 截 16 位 | — |
topo_sort NEW | (units[]) → {ok, order, cycle?} | DAG 拓扑排序 + 环检测 | — |
write_units_json NEW | (pid, units) | typebox 校验 + 写盘 + 自动 hash 计算 | units.json 写 |
write_integration_spec NEW | (pid, spec) | 校验 involves ↔ units → 写盘 | integration/spec.json 写 |
diff_units NEW | (pid, old, new) → {added, changed, removed} | 对比两版 units.json | — |
sdd_section_patcher NEW | (pid, patch) | patch 模式专用:精准应用 triage 提议 | sdd/ 写 |
validate_sdd_consistency NEW | (pid) → {ok, issues[]} | SDD ↔ units.json ↔ spec 三方一致性 | — |
update_search_index | (pid, target='sdd') | 同 PM 工具,参数指定 sdd 子站 | — |
archive_doc | (pid, kind, oldFile) | 归档到 history/sdd-rev-N/ | — |
write_todolist | (items) | 同 PM | agent-state/ |
update_todo_item | (id, status, output_ref?) | 同 PM | agent-state/ |
update_runtime_state | (step, total) | 同 PM | agent-state/ |
timeline_append | (kind, ref, summary, payload?) | 同 PM | — |
record_memory | (topic, content, relevance) | 同 PM,写 memory/devloop/architect/ | memory/devloop/architect/ |
2.4 禁用的工具 / 行为
- 不允许 Bash — Architect 不跑命令;所有操作走专用工具
- 不允许 Edit — 章节级写就行,不需要细粒度行 patch
- 不允许 call_sub_agent
- 不允许写:
prd/·units/*/code/·units/*/tests/·units/*/contract.json(Coder 的领地) ·integration/runs/(Integrator 的领地)
③ 持久化集成(M-10/11/12/13 接入)
与 PM Agent 同模式:开工前 LoopController 注入 memory,结束前可写新 memory。todolist + runtime state + timeline 在每个 step 进出时同步更新。
3.1 Architect 写 memory 的判断
仅在架构模式非平凡时记录:
- ✓ "用户偏 SQL DDL 写在 db Unit 而非 be Unit"
- ✓ "中型项目 mw 拆分阈值:≥3 个 Unit 共享时拆"
- ✗ 不记录具体项目的 schema 细节(那是 PRD/SDD 内容)
- ✗ 不记录具体 Unit 命名(feature-tree 已固化)
④ I/O 契约
4.1 输入 schema
const ArchitectInput = Type.Object({ pid: Type.String(), mode: Type.Union([Type.Literal('generate'), Type.Literal('patch'), Type.Literal('resume')]), // generate 模式 prd_rev: Type.Optional(Type.Number()), // patch 模式 triage: Type.Optional(Type.Object({ run_id: Type.String(), proposed_sdd_patch: Type.Object({ target_section: Type.String(), op: Type.String(), text: Type.String(), }), affected_units: Type.Array(Type.String()), category: Type.String(), confidence: Type.Number(), })), // resume 模式 resume_token: Type.Optional(Type.String()), // 自动注入 call_id: Type.String(), company_id: Type.String(), });
4.2 输出 schema
const ArchitectOutput = Type.Object({ ok: Type.Boolean(), mode: Type.String(), sdd_rev: Type.Number(), files_written: Type.Array(Type.String()), files_archived: Type.Array(Type.String()), units_diff: Type.Object({ added: Type.Array(Type.String()), changed: Type.Array(Type.String()), // contract_hash 变化 removed: Type.Array(Type.String()), }), units_count: Type.Number(), integration_cases_count: Type.Number(), validation: Type.Object({ ok: Type.Boolean(), issues: Type.Array(Type.String()), }), change_summary: Type.String({ maxLength: 500 }), memory_recorded: Type.Array(Type.Object({ topic, relevance })), duration_ms: Type.Number(), tokens_used: Type.Number(), });
⑤ 上下游传递
⬆ 上游
- PM Agent · 通过 prd/ 站点 + changelog.ndjson
- BugTriager · 通过 triage.json(patch 模式)
- LoopController · onHumanFeedback 后 / onL2Failure 后 callAgent
- M-11 Memory · 启动时由 LoopController 注入
- 历史 sdd/ + units.json · 读以决定增量 vs 重写
⬇ 下游
- Coder Agent · 读 sdd/mvcp/*.html + Unit spec 写代码
- Tester Agent · 读 sdd/mvcp/ tests 列表派生 vitest 用例
- Integrator Agent · 读 integration/spec.json 跑 e2e
- BugTriager Agent · 读 sdd/ + units.json 做归因
- UnitRegistry · 读 units_diff 决定哪些 Unit reset 到 pending
- LoopController · 读 units_diff 调度 L1 重跑
- SSE · sdd.updated / agent.* · changelog.ndjson append
⑥ 完整执行样例
# ─────────── INPUT ─────────── { "pid": "p-731042", "mode": "generate", "prd_rev": 1, "call_id": "call-20260513-1031-a1b2", "company_id": "c-lemonv-001" } # ─────────── INJECTED MEMORY ─────────── ## Memory (from prior projects) ### sql-conventions (relevance: 0.91) - SQL DDL 偏好写在独立 db Unit (跨 be 复用) - better-sqlite3 是默认选择,不用 mysql/postgres ### auth-patterns (relevance: 0.85) - 认证逻辑只要不止 2 处用到就拆 mw Unit - JWT 在 Authorization header, 不存 cookie # ─────────── 内部执行 ─────────── # step 0 · 读 prd/feature/feature-tree.html # 发现 2 个特性: # F1: TODO 列表 API (POST /todos, GET /todos) # F2: TODO Vue 前端 # step 1 · write_todolist (12 项 generate 模板) # step 2 · 拆 Unit (架构判断): # F1 → todo-be · 单独 schema 较简单 → 暂不拆 db Unit (写在 be 内) # (后续 F1 复杂化或加缓存才拆) # F2 → todo-fe # 无认证 → 不拆 mw # step 3 · 起草 contracts todo-be contract: POST /api/todos { title: string } → 201 { id, title, created_at } | 400 { error } GET /api/todos → 200 { items: TodoItem[] } todo-fe contract: Component: TodoListPage Props: {} Emits: {} consumes: todo-be (HTTP) # step 4 · hash_contract hash_contract(todo-be.contract) → "sha256:4a7c8e2f9d1b3a5e" hash_contract(todo-fe.contract) → "sha256:b2d4f6a8e0c1d3f5" # step 5 · DAG todo-be: depends_on [] todo-fe: depends_on ["todo-be"] topo_sort → ok, order=[todo-be, todo-fe] # step 6 · write_mvcp_page('model', defs) # model.html 含 TodoItem schema + db schema 内嵌注释 # step 7-10 · 写 view/control/process # step 11 · write_units_json { "project_id": "p-731042", "prd_rev": "1", "sdd_rev": "1", "units": [ { "id":"todo-be", "type":"be", "status":"pending", "depends_on":[], "retries":0, "retry_budget":5, "contract_hash":"sha256:4a7c8e2f9d1b3a5e" }, { "id":"todo-fe", "type":"fe", "status":"pending", "depends_on":["todo-be"], "contract_hash":"sha256:b2d4f6a8e0c1d3f5" } ], "l2_rounds":0, "l2_budget":3 } # step 12 · write_integration_spec { "version": "sdd-rev-1", "cases": [ { "id": "create-then-list", "steps": [ { "unit":"todo-be", "call":"POST /api/todos", "body":{"title":"buy milk"}, "expect":{"status":201} }, { "unit":"todo-be", "call":"GET /api/todos", "expect":{"status":200, "json":{"items":"$.length > 0"}} } ], "involves": ["todo-be"] }, { "id": "ui-renders-from-api", "steps": [ { "unit":"todo-be", "call":"POST /api/todos", ... }, { "unit":"todo-fe", "call":"mount <TodoListPage/>", "expect":{"dom":"li:contains('buy milk')"} } ], "involves": ["todo-be", "todo-fe"] } ] } # step 13 · validate_sdd_consistency → ok=true # step 14 · timeline_append("sdd_generated", "sdd@rev1", "2 units, 2 cases") # ─────────── OUTPUT ─────────── { "ok": true, "mode": "generate", "sdd_rev": 1, "files_written": [ "sdd/index.html", "sdd/overview.html", "sdd/assets/shell.css", "sdd/assets/shell.js", "sdd/assets/search-index.json", "sdd/mvcp/model.html", "sdd/mvcp/view.html", "sdd/mvcp/control.html", "sdd/mvcp/process.html", "units.json", "integration/spec.json" ], "units_diff": { "added":["todo-be","todo-fe"], "changed":[], "removed":[] }, "units_count": 2, "integration_cases_count": 2, "validation": { "ok":true, "issues":[] }, "change_summary": "SDD rev 1: 2 units (todo-be · todo-fe), 2 e2e cases", "memory_recorded": [], "duration_ms": 68_120, "tokens_used": 14_823 }
# ─────────── INPUT (patch 模式) ─────────── { "pid": "p-731042", "mode": "patch", "triage": { "run_id": "run-int-20260513-1031", "proposed_sdd_patch": { "target_section": "#unit-auth-be", "op": "update_contract", "text": "明确:登录失败必须 401 with {error:'invalid_credentials'}" }, "affected_units": ["auth-be", "auth-fe"], "category": "contract_mismatch", "confidence": 0.86 }, ... } # step 1 · 读 triage + 定位 sdd/mvcp/control.html 内 data-def-name="auth-be" section # step 2 · sdd_section_patcher.apply() # 在 #unit-auth-be 的 contract 段落更新 401 响应描述 # 同时同步 #unit-auth-fe 内对 401 期望的描述(因为 contract_mismatch 是双侧) # step 3 · 重算 contract_hash # auth-be.contract 文本变了 → 新 hash sha256:8e1f... # auth-fe.contract 文本变了 → 新 hash sha256:9c4d... # step 4 · write_units_json (incremental patch) # 找到 units[].id == 'auth-be' → 更新 contract_hash # 找到 units[].id == 'auth-fe' → 更新 contract_hash # 其他 unit 不动 # step 5 · integration/spec.json 不动 (triage 没要求改) # step 6 · validate_sdd_consistency → ok=true # timeline_append("sdd_patched", "sdd@rev2", # "+ 401 invalid_credentials · affected: auth-be, auth-fe") # ─────────── OUTPUT ─────────── { "ok": true, "mode": "patch", "sdd_rev": 2, "files_written": [ "sdd/mvcp/control.html", // 仅这一页改 "units.json", "sdd/assets/search-index.json" ], "files_archived": [ "history/sdd-rev-1/control.html", "history/units-rev-1.json" ], "units_diff": { "added":[], "changed":["auth-be","auth-fe"], // contract_hash 变化 → UnitRegistry reset "removed":[] }, ... } # 下游连锁: # UnitRegistry 见 changed=[auth-be, auth-fe] # → 两个 Unit status: passed → pending # → LoopController 回 L1 重跑
# ─────────── INPUT (incremental,PRD 加了 OAuth) ─────────── { "pid": "p-731042", "mode": "generate", // PRD 变化 → 走 generate,不是 patch "prd_rev": 2, ... } # 注意: PRD 变化触发的 Architect 调用是 mode=generate (重新派生整套 SDD) # patch 模式专门给 BugTriager 用 # step 0 · 读 prd/feature/feature-tree.html (rev 2) # 原 2 特性 + 1 新特性 (OAuth) # step 2 · 拆 Unit # 原:todo-be / todo-fe (保留) # 新 OAuth → 多处复用认证(todo-be 也得保护) → 拆 mw # - auth-be: 处理 GitHub OAuth callback # - auth-fe: 登录按钮 + redirect 处理 # - auth-mw: JWT 校验中间件 (todo-be 会用到) # step 5 · DAG # todo-be : depends_on [] # auth-be : depends_on [] # auth-mw : depends_on [] # todo-be 现在依赖 auth-mw → 加 dependency: depends_on:["auth-mw"] # todo-fe : depends_on ["todo-be","auth-fe"] # auth-fe : depends_on ["auth-be"] # topo_sort → ok # diff_units(old, new): # added: ["auth-be","auth-fe","auth-mw"] # changed: ["todo-be"] ← 加了 auth-mw 依赖,contract 变了 # removed: [] # OUTPUT { "ok": true, "mode": "generate", "sdd_rev": 3, "units_diff": { "added": ["auth-be", "auth-fe", "auth-mw"], "changed": ["todo-be"], // 受 auth-mw 添加影响 "removed": [] }, "units_count": 5, "integration_cases_count": 5, // 新加 3 个 oauth flow + 1 个回归 "change_summary": "+ OAuth 3 units · todo-be 加 auth-mw 依赖", "memory_recorded": [ { "topic":"oauth-arch", "relevance":0.72 } ] } # UnitRegistry: # added [auth-be, auth-fe, auth-mw] → status: pending (入 L1 队列) # changed [todo-be] → status: passed → pending (重新跑) # todo-fe 不在 diff → status 保持 (passed 的不动)
⑦ 助手工具源码(4 个)
7.1 hash-contract.js · 固定算法 contract hash
// server/services/agent-tools/hash-contract.js · ~40 行 const crypto = require('crypto'); /** * Canonical JSON:递归排序 key,统一引号,无空白。 * 保证同样 contract 在任意机器/任意时间产生同样 hash。 */ function canonical(v) { if (v === null || typeof v !== 'object') return JSON.stringify(v); if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']'; const keys = Object.keys(v).sort(); return '{' + keys.map(k => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}'; } function hashContract(contract) { if (!contract || typeof contract !== 'object') throw new Error('hashContract: contract must be object'); const body = contract.exports || contract; const normalized = canonical(body); const full = crypto.createHash('sha256').update(normalized).digest('hex'); return 'sha256:' + full.slice(0, 16); // 截 16 hex chars } /** * 验证某 Unit 的 contract_hash 是否与文件中标记一致。 * 用于 self-check + L2 gate 判定。 */ function verifyContractHash(unit, contract) { const recomputed = hashContract(contract); return { ok: recomputed === unit.contract_hash, expected: unit.contract_hash, actual: recomputed, }; } module.exports = { hashContract, verifyContractHash, canonical };
7.2 unit-extractor.js · 从 PRD feature-tree 派生 Unit 草案
// server/services/agent-tools/unit-extractor.js · ~80 行 const cheerio = require('cheerio'); const fs = require('fs'); /** * 读 prd/feature/feature-tree.html,识别每个 feature, * 给 Architect 返回 Unit 拆分草案(架构判断仍由 Agent 做)。 */ function extractUnitDrafts(prdFeatureTreePath) { const $ = cheerio.load(fs.readFileSync(prdFeatureTreePath, 'utf8')); const drafts = []; // feature-tree 约定: 每个特性是一个 <section data-feature-id> $('section[data-feature-id]').each((_, el) => { const id = $(el).attr('data-feature-id'); const name = $(el).find('h3').first().text().trim(); const desc = $(el).find('p').first().text().trim(); // 启发式判断需要哪些 Unit type const hint = $(el).attr('data-needs') || ''; // "be,fe" 或 "be,fe,mw,db" const needs = hint.split(',').map(s => s.trim()).filter(Boolean); const defaultNeeds = needs.length ? needs : ['be', 'fe']; // 默认全栈 for (const type of defaultNeeds) { drafts.push({ unit_id: `${id}-${type}`, feature_id: id, feature_name: name, type, description: desc, suggested_depends: suggestDeps(id, type, defaultNeeds), }); } }); return drafts; } function suggestDeps(featureId, type, allNeeds) { // fe 默认依赖同 feature 的 be if (type === 'fe' && allNeeds.includes('be')) return [`${featureId}-be`]; // be 默认无依赖 (Architect 后续可加 mw/db 依赖) return []; } function validateUnitId(id) { return /^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/.test(id); } module.exports = { extractUnitDrafts, validateUnitId };
7.3 sdd-section-patcher.js · patch 模式精准应用
// server/services/agent-tools/sdd-section-patcher.js · ~70 行 const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); /** * 应用 triage.proposed_sdd_patch 到 sdd/mvcp/ 内对应文件。 * 不重写整页,只替换 target_section 的内容。 */ function applySddPatch({ sddDir, patch, by }) { // target_section 形如 "#unit-auth-be" 或 "#def-auth-be-contract" // 在 sdd/mvcp/*.html 中查找含有该 id (或 data-unit-id 匹配) 的 section const candidates = ['model.html', 'view.html', 'control.html', 'process.html']; let applied = []; for (const f of candidates) { const p = path.join(sddDir, 'mvcp', f); if (!fs.existsSync(p)) continue; const raw = fs.readFileSync(p, 'utf8'); const $ = cheerio.load(raw, { decodeEntities: false }); // 尝试匹配 target_section const selector = patch.target_section.replace('#unit-', 'section[data-unit-id="').replace(/$/, '"]'); const target = $(selector); if (target.length === 0) continue; if (patch.op === 'update_contract') { const pre = target.find('pre[data-contract]').first(); if (pre.length === 0) { throw new Error(`${f}: target section has no <pre data-contract>`); } // 追加 patch.text 作为新行 (而非替换,以保留历史) pre.append('\n // PATCH (' + by + '):\n ' + patch.text + '\n'); // 升 section data-rev const curRev = parseInt(target.attr('data-rev') || '1', 10); target.attr('data-rev', String(curRev + 1)); fs.writeFileSync(p, $.html()); applied.push({ file: f, section: patch.target_section, new_rev: curRev + 1 }); } else if (patch.op === 'append_acceptance') { const ul = target.find('ul[data-acceptance]').first(); ul.append('<li>' + patch.text + '</li>'); fs.writeFileSync(p, $.html()); applied.push({ file: f, op: 'append_acceptance' }); } break; // 第一个匹配的文件就处理 } if (applied.length === 0) throw new Error(`No matching target_section found in sdd/mvcp/`); return applied; } module.exports = { applySddPatch };
7.4 sdd-validator.js · 三方一致性校验
// server/services/agent-tools/sdd-validator.js · ~90 行 const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); const { validateUnitId } = require('./unit-extractor'); const { hashContract } = require('./hash-contract'); function topoSort(units) { const idx = new Map(); units.forEach((u,i) => idx.set(u.id, i)); const color = new Array(units.length).fill(0); const order = []; let hasCycle = false; function visit(i) { if (color[i] === 2) return; if (color[i] === 1) { hasCycle = true; return; } color[i] = 1; for (const dep of units[i].depends_on) { if (idx.has(dep)) visit(idx.get(dep)); } color[i] = 2; order.push(units[i].id); } for (let i = 0; i < units.length; i++) visit(i); return { ok: !hasCycle, order, cycle: hasCycle }; } function validateSddConsistency(pid) { const root = path.resolve(projectsDir, pid); const issues = []; // 1. units.json 读 + schema (typebox 见 M-2) const units = JSON.parse(fs.readFileSync(path.join(root, 'units.json'), 'utf8')); units.units.forEach(u => { if (!validateUnitId(u.id)) issues.push(`unit id "${u.id}" violates naming pattern`); }); // 2. DAG 无环 const topo = topoSort(units.units); if (topo.cycle) issues.push('units.json has cycle in depends_on'); // 3. 每个 Unit 在 sdd/mvcp/ 至少出现 1 次 const sddTexts = ['model', 'view', 'control', 'process'] .map(k => { const p = path.join(root, 'sdd/mvcp', k+'.html'); return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : ''; }).join('\n'); for (const u of units.units) { if (!sddTexts.includes(`data-unit-id="${u.id}"`) && !sddTexts.includes(`data-def-name="${u.id}"`)) issues.push(`unit ${u.id} not referenced in sdd/mvcp/`); } // 4. integration/spec.json involves 都在 units const spec = JSON.parse(fs.readFileSync(path.join(root, 'integration/spec.json'), 'utf8')); const unitIds = new Set(units.units.map(u => u.id)); for (const c of spec.cases) { for (const u of c.involves) { if (!unitIds.has(u)) issues.push(`integration case ${c.id} involves unknown unit ${u}`); } } return { ok: issues.length === 0, issues }; } module.exports = { validateSddConsistency, topoSort };
⑧ 自检 Checklist · 完工前必跑
| # | 检查项 | 命令 / 工具 | 通过判定 |
|---|---|---|---|
| 1 | 三方一致性 | validate_sdd_consistency(pid) | ok=true, issues=[] |
| 2 | units.json schema | typebox 校验 | 合规 |
| 3 | DAG 无环 | topo_sort | cycle=false |
| 4 | 每个 Unit 在 sdd/mvcp/ 有引用 | cheerio grep | 全 hit |
| 5 | 每个 Unit contract_hash 重算一致 | verifyContractHash | 全 ok=true |
| 6 | Unit id 全部符合命名规则 | validateUnitId | 全 true |
| 7 | integration involves ⊆ units | validate_sdd_consistency 内置 | 无 "unknown unit" |
| 8 | SDD 站点搜索索引同步 | update_search_index(pid, 'sdd') | pages 含新写文件 |
| 9 | 每页 ≥4 类信息 | cheerio 同 PRD 校验 | 无 "X/8 types" |
| 10 | todolist 全 done | read agent-state/architect-todo.json | 全 done |
| 11 | timeline 含 sdd_generated/sdd_patched | tail timeline.ndjson | 最后 5 条之一命中 |
| 12 | agent-state.architect.json 终态 | read M-10 | status=done |
| 13 | changelog.ndjson 含 sdd_rev 新条目 | tail changelog.ndjson | kind='sdd_rev' |
| 14 | patch 模式专用:仅 affected_units 的 hash 变化 | diff_units 对比 | changed ⊆ triage.affected_units |
任一不过 → 不输出 ok=true。patch 后重跑 ≤2 次。第 3 次仍不过 → 报 validation_failed 上报 L3。
⑨ 失败模式与降级
| 场景 | 检测 | 降级 |
|---|---|---|
| PRD 描述歧义(feature 无法明确拆 Unit) | Architect 自识别 | SDD 加 <aside data-question>,触发 L3 追问 |
| Unit 拆得不一致(同一 feature 之前 2 个 Unit、现在 3 个) | L2 修复 ≥2 轮时 | 强制重生整个 SDD(generate 模式),不再 patch |
| DAG 出环 | topo_sort | 报错 → Agent retry 一次(移除最新加的依赖);2 次仍环 → blocked |
| contract_hash 碰撞(极低概率) | 所有 hash 集合查重 | 退化用完整 64 位 hex |
| patch target_section 找不到 | sdd_section_patcher 报错 | 升级到 generate 模式重生 |
| integration/spec.json 引用未知 unit | validate 内置 | 修 spec 或 SDD,retry |
| Unit 命名违规(输入了 AuthBE) | validateUnitId | 直接拒,Agent 必须改名 |
| cheerio 解析 sdd/ html 失败 | write_mvcp_page 工具内置 | 不写盘,retry 一次重生这段 |
| memory 注入太多挤爆 prompt | LoopController 控制 top 5 | — |
| generate 模式遇到现有 units.json | Agent 自识 | 归档现有 → 重生 → 用 diff_units 算 added/changed/removed |
⑩ 与其他 Agent 的握手摘要
| 对方 | Architect 给的 | Architect 收的 |
|---|---|---|
| PM | (无;PM 不读 SDD) | prd/ 站点(generate 模式) |
| BugTriager | (无) | triage.json(patch 模式) |
| Coder | sdd/mvcp/*.html · 每 Unit 的 spec + contract 描述 | (无;Architect 不读 code) |
| Tester | sdd/mvcp/*.html 的 <ul class="tests"> 列表 | (无) |
| Integrator | integration/spec.json | (无) |
| UnitRegistry | units.json + units_diff | (无) |
| LoopController | ArchitectOutput JSON | ArchitectInput JSON · triage |
| memory/devloop/architect/ | 新 .md 文件 | 启动时被 LoopController 注入 |
⑪ 「测试 Architect Agent 是否真能跑」的实战命令
本规范的验证终局:
# 前置: PM Agent 已跑通,产出 projects/p-test001/prd/ 完整站点 # 1. 注册 Agent 模板 # 把本页 §2.2 的 System Prompt 落到: # system/agents/devloop-architect/prompt.md # 并在 system/agents/_registry.json 加 'devloop-architect' 条目 # 2. 准备输入 (单行紧凑 JSON) echo '{"pid":"p-test001","mode":"generate","prd_rev":1,"call_id":"call-arch-test","company_id":"c-test"}' > /tmp/arch-test.json # 3. 调 Architect curl -X POST http://localhost:3010/api/devloop/_test/run-agent/architect \ -H "Content-Type: application/json" \ -d @/tmp/arch-test.json # 4. 期望:60-120 秒内 # data/companies/c-test/projects/p-test001/ # ├── sdd/index.html, sdd/overview.html, sdd/assets/*, sdd/mvcp/{model,view,control,process}.html # ├── units.json (DAG) # └── integration/spec.json # 5. 三方一致性校验 node -e "console.log(require('./server/services/agent-tools/sdd-validator').validateSddConsistency('p-test001'))" # 期望: { ok: true, issues: [] } # 6. DAG 拓扑验证 node -e "const u=require('./data/companies/c-test/projects/p-test001/units.json');console.log(require('./server/services/agent-tools/sdd-validator').topoSort(u.units))" # 期望: { ok: true, order: [...], cycle: false } # 7. 浏览器打开 # file:///data/companies/c-test/projects/p-test001/sdd/index.html # 左侧 3 级菜单, 4 个 MVCP 章节, Ctrl+K 可搜索每个 Unit 名 # 8. (可选) patch 模式测试 echo '{"pid":"p-test001","mode":"patch","triage":{"run_id":"fake","proposed_sdd_patch":{"target_section":"#unit-todo-be","op":"update_contract","text":"GET /api/todos 加分页 ?page=N"},"affected_units":["todo-be","todo-fe"],"category":"contract_mismatch","confidence":0.9},"call_id":"call-arch-patch","company_id":"c-test"}' > /tmp/arch-patch.json curl -X POST http://localhost:3010/api/devloop/_test/run-agent/architect -d @/tmp/arch-patch.json # 期望: 仅 sdd/mvcp/control.html · units.json 改 # units_diff.changed = ['todo-be','todo-fe'] # todo-be / todo-fe contract_hash 变化, 其他 Unit hash 不变
这 8 条全过 = Architect Agent skill 验证通过。配合上一页 PM 的 §⑪,二者通过即代表 P1 + P3 阶段的核心 Agent 都已就绪,可以进 Phase 4 写其他 4 个 Agent。