① LoopController
路径:server/src/core/loop-controller.js · ~180 行
编排 L1 / L2 / L3 三层状态机。所有 Agent 调用统一从这里出。它本身无状态,所有状态从 UnitRegistry / DocStore 读。
关键方法
| 方法 | 调用时机 | 返回 |
|---|---|---|
runProject(pid) | 启动 / 续跑项目 | 'shipped' | 'blocked' | 'l2_exhausted' |
runUnit(unit) | L1 内部 | UnitResult |
onHumanFeedback(pid, req) | L3 入口(HTTP) | void · 异步触发 runProject |
onL2Failure(pid, runId) | Integrator 返回 fail 后 | void |
getState(pid) | Dashboard 轮询 | ProjectState 快照 |
主循环骨架
async function runProject(pid) { while (true) { // L1: pending → coding → testing → passed const waves = scheduler.getExecutionGroups(unitRegistry.pending(pid)); for (const w of waves) await Promise.all(w.map(runUnit)); if (unitRegistry.anyBlocked(pid)) return 'blocked'; // L2 gate if (!l2GateOpen(pid)) continue; const r = await agentManager.callAgent('integrator', { pid }); if (r.ok) return 'shipped'; // L2 failed → triage → patch SDD → reset units const tri = await agentManager.callAgent('bug-triager', r.failure); if (tri.confidence < 0.5) { sse.emit(pid, 'human_intervention_needed', tri); return 'l2_blocked'; } await agentManager.callAgent('architect', { mode: 'patch', patch: tri.proposed_sdd_patch }); unitRegistry.reset(pid, tri.affected_units); if (project.l2Rounds++ >= 3) return 'l2_exhausted'; } }
② DocStore
路径:server/services/doc-store.js · ~140 行
负责 prd.html / sdd.html / units.json / integration/spec.json 的读写、归档、校验。所有写操作走它,没有 Agent 能直接 Write 这些文件。
API 面
module.exports = { // 读 readPRD(pid), readSDD(pid), readUnits(pid), readIntegrationSpec(pid), // 写(自动归档旧版) writePRD(pid, html), writeSDD(pid, html), writeUnits(pid, json), writeIntegrationSpec(pid, json), // 校验 validateHTML(html), // 用 cheerio + 必含 section validateUnitsSchema(json), // 用 typebox // diff / 归档 diffPRD(pid, fromRev, toRev), archive(pid, kind, oldHtml), // → history/{kind}-rev-N.html };
HTML 校验规则
| 文档 | 必含元素 | 校验工具 |
|---|---|---|
| prd.html | 顶级 <article data-doc="prd" data-rev>,至少 1 <section> | cheerio |
| sdd.html | 每 <section data-unit-id> 含 <h3> + <pre data-contract> | cheerio |
| units.json | schema 校验 + DAG 无环 | typebox + 拓扑排序 |
| integration/spec.json | 每 case 的 involves 都在 units.json 中存在 | 自写 |
③ UnitRegistry
路径:server/services/unit-registry.js · ~120 行
Unit 状态机的 source of truth。状态转换都走它,不允许 Agent 自己改 units.json 的 status 字段。
API 面
module.exports = { list(pid), // 全部 unit + 状态快照 pending(pid), passed(pid), blocked(pid), l2GateOpen(pid), // 全 passed 且 hash 一致 // 状态转换 markCoding(pid, unitId), markTesting(pid, unitId), pass(pid, unitId, contractHash), fail(pid, unitId, reason), block(pid, unitId, reason), reset(pid, unitIds), // L2 修复后 // diff (用于 L3 增量) diffWithSDD(pid), // → { added, changed, removed } };
状态转换合法性
| from \ to | pending | coding | testing | failed | passed | blocked |
|---|---|---|---|---|---|---|
| pending | — | ✓ | ||||
| coding | — | ✓ | ||||
| testing | — | ✓ | ✓ | |||
| failed | ✓ | — | ✓ | |||
| passed | ✓ (L2 reset) | — | ||||
| blocked | ✓ (人类干预) | — |
非法转换 → 抛异常 + SSE 错误事件,便于排查。
三者协作
外部依赖
| 包 | 用途 | 是否新增 |
|---|---|---|
cheerio | 解析 / 校验 HTML | 新增 (~120KB) |
@sinclair/typebox | JSON schema 校验 | 已存在 |
better-sqlite3 | — | 已存在(不需要新表) |
不引入新数据库;项目状态全部落文件。Dashboard 用 SSE 推送,不需要轮询 DB。