状态机
L2 闸门触发条件
不是「所有 Unit 都跑过一次」,而是「所有 Unit 的当前 contract_hash 与依赖关系下,状态都是 passed」。这避免 SDD 改过之后某个旧版 Unit 还停留在 passed 而事实上契约已变。
function l2GateOpen(project) { return project.units.every(u => u.status === "passed" && u.contract_hash === currentSDD(project).contractHashFor(u.id) ); }
失败归因 — BugTriager 输出契约
BugTriager 必须输出一个机读 JSON,LoopController 据此自动决定回滚范围。Triager 不直接改文件,只发「建议」。
{ "run_id": "run-int-20260512-1031", "root_cause": "auth-be returns 500 on invalid pwd, expected 401; auth-fe error handler relies on 401", "category": "contract_mismatch", "affected_units": ["auth-be", "auth-fe"], "proposed_sdd_patch": { "target_section": "#unit-auth-be", "op": "update", "text": "明确:登录失败必须 401 with {error:'invalid_credentials'}" }, "confidence": 0.82 }
归因 → 回写 → 重置 三步
① 归因
BugTriager 读 stderr + diff + units.json + sdd.html,产 triage.json。confidence < 0.5 时上报 L3 让人类裁决。
② 回写
Architect 接收 proposed_sdd_patch,用 section 级 DOM 操作改 sdd.html,归档旧版到 history/sdd-rev-N.html。
③ 重置
UnitRegistry 把 affected_units 的 status 改回 pending,并自动重算 contract_hash,触发 L1 新一轮。
L2 修复预算
同一项目每个 PRD rev 内最多触发 3 次 L2 修复轮回。超出后整个项目挂起,发 SSE 事件 project.l2_exhausted,等待人类(L3)发新需求或手动干预。
为什么是 3:实测 Claude Code Agent 三轮内还修不好的,要么是契约描述本身有歧义(PRD 缺陷),要么是技术选型有根本问题。再加轮次只会浪费 Token。
集成测试套件从哪来?
这是一个设计决策(见 风险与待决 · R3)。初版选 方案 A:
| 方案 | 谁写集成测试 | 优点 | 缺点 |
|---|---|---|---|
| A · 选中 | Architect 在生成 SDD 时同步生成 integration/spec.json | 少一个 Agent,集成测试与 SDD 同步演化 | Architect 职责变重 |
| B | 独立 IntegrationAuthor Agent | 关注点分离 | 多一个 Agent,多一次握手 |
| C | Integrator 自己写 | 读取代码后再写最贴合 | 容易写一个「永远会通过」的 trivial test |