Commit 53eda8d2d677cfd49700a38579286f366fc7363d

Authored by yibo
1 parent fd86ebf1

chore(prd): snapshot build-manifest.json (52 MVCP defs, 100% extraction)

Output of scripts/extract-manifest.js against docs/prd/newfunction/.
Captures the current state:
- 52 MVCP definitions (model=15, view=11, control=17, process=9)
- 100% path + shape extraction coverage
- Sized ~52KB

This file is regenerable (just run the script), but committing the
snapshot lets reviewers and external Claude Code sessions diff the
schema surface without rebuilding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing 1 changed file with 570 additions and 0 deletions
build-manifest.json 0 → 100644
  1 +{
  2 + "meta": {
  3 + "source": "docs/prd/newfunction",
  4 + "prdRev": "v0.2",
  5 + "extractedAt": "2026-05-13T04:16:35.145Z"
  6 + },
  7 + "model": [
  8 + {
  9 + "name": "ProjectMeta",
  10 + "path": "data/companies/{cid}/projects/{pid}/project.json",
  11 + "shape": "// TypeBox schema\nconst ProjectMeta = Type.Object({\n id: Type.String({ pattern: '^p-[a-z0-9]{6}$' }),\n company_id: Type.String(),\n name: Type.String(),\n status: Type.Union([\n Type.Literal('prd_drafting'),\n Type.Literal('running'),\n Type.Literal('blocked'),\n Type.Literal('shipped'),\n Type.Literal('shipped-final'),\n Type.Literal('abandoned'),\n ]),\n created_at: Type.String({ format: 'date-time' }),\n updated_at: Type.String({ format: 'date-time' }),\n owner: Type.String(),\n token_budget: Type.Number({ default: 5_000_000 }),\n token_used: Type.Number({ default: 0 }),\n compressOldRuns: Type.Boolean({ default: false }),\n});",
  12 + "contract": [
  13 + "id 全局唯一,跨 company 不重复",
  14 + "status 只能按状态机合法转换(见 sdd-process.html · M-process-project)",
  15 + "token_used > token_budget 时 LoopController 自动挂起项目"
  16 + ],
  17 + "tests": []
  18 + },
  19 + {
  20 + "name": "UnitsJson",
  21 + "path": "data/companies/{cid}/projects/{pid}/units.json",
  22 + "shape": "const Unit = Type.Object({\n id: Type.String({ pattern: '^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$' }),\n type: Type.Union([Type.Literal('be'), Type.Literal('fe'), Type.Literal('mw'), Type.Literal('db')]),\n status: Type.Union([\n Type.Literal('pending'), Type.Literal('coding'),\n Type.Literal('testing'), Type.Literal('failed'),\n Type.Literal('passed'), Type.Literal('blocked'),\n ]),\n depends_on: Type.Array(Type.String()),\n retries: Type.Number({ default: 0 }),\n retry_budget: Type.Number({ default: 5 }),\n contract_hash: Type.String({ pattern: '^sha256:[a-f0-9]{16}$' }),\n last_run: Type.Optional(Type.String()),\n});\nconst UnitsJson = Type.Object({\n project_id: Type.String(),\n prd_rev: Type.String(),\n sdd_rev: Type.String(),\n units: Type.Array(Unit),\n l2_rounds: Type.Number({ default: 0 }),\n l2_budget: Type.Number({ default: 3 }),\n});",
  23 + "contract": [
  24 + "DAG 必须无环(DocStore 写入前用拓扑排序校验)",
  25 + "depends_on 引用的 id 必须存在于本文件 units 中",
  26 + "contract_hash = sha256(SDD 中对应 section <pre data-contract> 文本) 截前 16 位"
  27 + ],
  28 + "tests": [
  29 + "循环依赖 → 拒绝写入",
  30 + "未知 id → 拒绝写入",
  31 + "所有 status 为 passed 且 hash 与当前 SDD 匹配 → l2GateOpen 返回 true"
  32 + ]
  33 + },
  34 + {
  35 + "name": "PRD-Site",
  36 + "path": "data/companies/{cid}/projects/{pid}/prd/ (目录)",
  37 + "shape": "// 目录布局 (PM Agent 产出,见 agents-pm.html §2.1)\nprd/\n├── index.html // Hero + reading guide entry\n├── reading-guide.html\n├── assets/\n│ ├── shell.css // 复制自模板\n│ ├── shell.js // 3 级菜单 + 搜索\n│ └── search-index.json\n├── product/\n│ ├── background.html\n│ ├── personas.html\n│ ├── original-req.html\n│ ├── scope.html\n│ ├── success-metrics.html\n│ └── glossary.html\n├── feature/\n│ └── feature-tree.html\n├── flow/\n│ └── user-flows.html\n└── prototype/\n └── proto-{viewName}.html\n\n// 每个 chapter HTML 内的结构 (cheerio 校验):\n<article data-doc=\"prd\" data-rev=\"N\">\n <section data-rev=\"M\" data-origin=\"{user@iso}\">\n <h3>...</h3>\n <p>...</p>\n </section>\n <aside data-conflict>...</aside> // 可选,index.html only\n <footer data-changelog>...</footer>\n</article>",
  38 + "contract": [
  39 + "顶级必须是 <article data-doc=\"prd\">,每页都有",
  40 + "每个 section 必含 <h3> + 至少一个 <p>",
  41 + "section data-rev 单调递增(页内),article data-rev 取该页内最大",
  42 + "每页至少覆盖 4/8 类 HTML 信息(见 agents-pm §2.2)",
  43 + "data-conflict aside 仅出现在 index.html 顶部,存在时整个项目暂停等人",
  44 + "11 个必备章节缺一个 → validate_prd_site 失败"
  45 + ],
  46 + "tests": [
  47 + "init 模式后 11 章节全在",
  48 + "incremental 模式后仅 affected 章节被改",
  49 + "多 rev 后 history/ 含旧版归档"
  50 + ]
  51 + },
  52 + {
  53 + "name": "SDD-HTML",
  54 + "path": "data/companies/{cid}/projects/{pid}/sdd.html",
  55 + "shape": "<article data-doc=\"sdd\" data-derived-from=\"prd@revN\">\n <section\n data-unit-id=\"auth-be\"\n data-type=\"be\"\n data-depends=\"\">\n <h3>Unit · auth-be</h3>\n <p>...功能描述...</p>\n <pre data-contract=\"openapi\">\n POST /api/auth/login ...\n </pre>\n <ul data-acceptance>\n <li>acceptance criteria 1</li>\n </ul>\n </section>\n</article>",
  56 + "contract": [
  57 + "每个 unit section 必含 contract + acceptance",
  58 + "data-depends 空格分隔 unit id 列表,可为空",
  59 + "data-derived-from 指向产生它的 prd 版本"
  60 + ],
  61 + "tests": []
  62 + },
  63 + {
  64 + "name": "ContractJson",
  65 + "path": ".../units/{unitId}/contract.json",
  66 + "shape": "const Contract = Type.Object({\n unit_id: Type.String(),\n version: Type.String({ pattern: '^sha256:' }),\n exports: Type.Object({\n http: Type.Optional(Type.Array(HttpEndpoint)),\n ts_types: Type.Optional(Type.String()),\n events: Type.Optional(Type.Array(EventSpec)),\n }),\n});\n\nconst HttpEndpoint = Type.Object({\n method: Type.String(),\n path: Type.String(),\n request: Type.Optional(Type.Object({}, { additionalProperties: true })),\n responses:Type.Object({}, { additionalProperties: true }),\n});",
  67 + "contract": [],
  68 + "tests": []
  69 + },
  70 + {
  71 + "name": "IntegrationSpec",
  72 + "path": ".../integration/spec.json",
  73 + "shape": "const IntegrationSpec = Type.Object({\n version: Type.String(),\n cases: Type.Array(Type.Object({\n id: Type.String(),\n steps: Type.Array(Type.Object({\n unit: Type.String(),\n call: Type.String(), // \"POST /api/x\"\n body: Type.Optional(Type.Any()),\n headers: Type.Optional(Type.Record(Type.String(), Type.String())),\n expect: Type.Object({\n status: Type.Number(),\n json: Type.Optional(Type.Any()),\n }),\n })),\n involves: Type.Array(Type.String()),\n })),\n});",
  74 + "contract": [
  75 + "involves 列表必须是 cases.steps.unit 的去重并集,BugTriager 用它做归因粗筛",
  76 + "headers / body 中可用 {{stepN.field}} 占位符引用前面步骤结果"
  77 + ],
  78 + "tests": []
  79 + },
  80 + {
  81 + "name": "RunResult",
  82 + "path": ".../units/{unitId}/runs/{runId}/result.json",
  83 + "shape": "const RunResult = Type.Object({\n run_id: Type.String({ pattern: '^run-\\\\d{8}-\\\\d{4}$' }),\n loop: Type.Literal('L1'),\n unit_id: Type.String(),\n agent: Type.Union([Type.Literal('coder'), Type.Literal('tester')]),\n attempt: Type.Number(),\n ok: Type.Boolean(),\n summary: Type.Object({\n total: Type.Number(),\n passed: Type.Number(),\n failed: Type.Number(),\n }),\n failures: Type.Array(Type.Object({\n test: Type.String(),\n expected: Type.Any(),\n got: Type.Any(),\n stack: Type.String(),\n hint: Type.String(),\n })),\n next_action: Type.Optional(Type.String()),\n retry_budget_left: Type.Optional(Type.Number()),\n duration_ms: Type.Number(),\n});",
  84 + "contract": [],
  85 + "tests": []
  86 + },
  87 + {
  88 + "name": "IntegrationResult",
  89 + "path": "data/companies/{cid}/projects/{pid}/integration/runs/{runId}/result.json",
  90 + "shape": "const IntResult = Type.Object({\n run_id: Type.String({ pattern: '^run-int-\\\\d{8}-\\\\d{4}$' }),\n ok: Type.Boolean(),\n failures: Type.Array(Type.Object({\n case: Type.String(),\n failed_step: Type.Number(),\n got: Type.Any(),\n involves: Type.Array(Type.String()),\n stderr_excerpt: Type.String(),\n })),\n process_log: Type.String(),\n duration_ms: Type.Number(),\n});",
  91 + "contract": [],
  92 + "tests": []
  93 + },
  94 + {
  95 + "name": "TriageJson",
  96 + "path": ".../integration/runs/{runId}/triage.json",
  97 + "shape": "const Triage = Type.Object({\n run_id: Type.String(),\n root_cause: Type.String({ minLength: 20 }),\n category: Type.Union([\n Type.Literal('contract_mismatch'),\n Type.Literal('impl_bug'),\n Type.Literal('spec_ambiguous'),\n Type.Literal('infra_flake'),\n ]),\n affected_units: Type.Array(Type.String()),\n proposed_sdd_patch: Type.Union([Type.Object({\n target_section: Type.String(),\n op: Type.String(),\n text: Type.String(),\n }), Type.Null()]),\n confidence: Type.Number({ minimum: 0, maximum: 1 }),\n evidence: Type.Array(Type.String()),\n});",
  98 + "contract": [
  99 + "category = contract_mismatch → proposed_sdd_patch 必填",
  100 + "category = impl_bug → proposed_sdd_patch 必为 null(不改 SDD,只 reset Unit)",
  101 + "confidence < 0.5 → LoopController 上报 L3 不自动应用",
  102 + "每条 evidence 必须在原文件中能 grep 到"
  103 + ],
  104 + "tests": []
  105 + },
  106 + {
  107 + "name": "AgentRuntimeState",
  108 + "path": "data/companies/{cid}/projects/{pid}/agent-state/{agent}.json",
  109 + "shape": "const AgentRuntimeState = Type.Object({\n agent: Type.Union([Type.Literal('pm'), Type.Literal('architect'),\n Type.Literal('coder'), Type.Literal('tester'),\n Type.Literal('integrator'), Type.Literal('bug-triager')]),\n unit_id: Type.Optional(Type.String()), // Coder/Tester 时填\n status: Type.Union([\n Type.Literal('idle'), Type.Literal('running'),\n Type.Literal('paused'), Type.Literal('done'),\n Type.Literal('error')\n ]),\n call_id: Type.String(), // 单次调用唯一 id\n current_step: Type.Number(), // 0-based, 引用 todolist.items[i]\n total_steps: Type.Number(),\n last_input_ref: Type.String(), // 输入快照路径\n last_output_ref: Type.Optional(Type.String()),\n session_id: Type.Optional(Type.String()), // ACP session 引用\n resume_token: Type.Optional(Type.String()), // 冷启恢复用\n started_at: Type.String({ format: 'date-time' }),\n updated_at: Type.String({ format: 'date-time' }),\n error: Type.Optional(Type.Object({\n code: Type.String(),\n message: Type.String(),\n stack: Type.Optional(Type.String())\n })),\n});",
  110 + "contract": [
  111 + "每次 LoopController.callAgent 前写一次(status=running)",
  112 + "Agent 自己在跨越主要 checkpoint 时通过 update_runtime_state 工具更新 current_step",
  113 + "调用结束(成功或失败)由 LoopController 写终态 status=done|error",
  114 + "Server SIGTERM 时所有 status=running 自动改 status=paused 并写 resume_token",
  115 + "Server 启动时扫所有 paused,发 SSE resume_decision_needed 等人决策"
  116 + ],
  117 + "tests": [
  118 + "Agent 崩溃 → state.json status=error,error 字段有 code/message",
  119 + "Server kill -9 → 重启后 state.json status=paused,resume_token 非空",
  120 + "Dashboard 能从 state.json 渲染「Architect at step 3/5」"
  121 + ]
  122 + },
  123 + {
  124 + "name": "AgentMemory",
  125 + "path": "data/companies/{cid}/memory/devloop/{agent}/{topic}.md",
  126 + "shape": "// Markdown + frontmatter,人可读 + 机器可读\n---\nagent: architect\ntopic: project-conventions\nproject_refs: [p-731042, p-845001]\nlast_updated: 2026-05-13T10:31:00Z\nrelevance_score: 0.92 # 0-1, agent 自评\nhits: 14 # 被注入 prompt 的次数\n---\n\n- 用户偏好 RESTful API + 完整 OpenAPI 注解\n- Token 续期实测:5s 内人类可接受\n- 认证方式:JWT 放 Authorization header,不用 cookie\n- 数据库统一用 better-sqlite3,不要 mysql/postgres",
  127 + "contract": [
  128 + "每个 Agent 启动时扫 memory/devloop/{agent}/*.md",
  129 + "按 relevance_score × recency 排序,取前 5 条注入 system prompt",
  130 + "Agent 自己可通过 record_memory(topic, content, relevance) 工具新增",
  131 + "Dedup:同 agent + 同 topic 是 upsert(覆盖 + 累加 hits),不新建文件。文件名 = {topic-slug}.md,topic-slug 由 record_memory 工具按规则计算(小写 · 空格转 -)",
  132 + "memory 跨项目(同 company 内)共享,不跨 company",
  133 + "frontmatter 必须含 agent / topic / last_updated 三字段",
  134 + "单文件 ≤ 8KB,超出 record_memory 自动 truncate 旧内容(保留新加 + 最相关)",
  135 + "v1 注意:如果初版 token 紧张可以关 memory 注入(top-5 改 top-0),完全跳过。memory 在 ≥ 5 个项目后才开始有积累价值。"
  136 + ],
  137 + "tests": [
  138 + "新 Agent 调用 → memory dir 存在则 system prompt 含 \"## Memory\" section",
  139 + "不存在该 agent dir → fallback 空 memory,不报错",
  140 + "record_memory 写入后下次启动能读到"
  141 + ]
  142 + },
  143 + {
  144 + "name": "AgentTodolist",
  145 + "path": "data/companies/{cid}/projects/{pid}/agent-state/{agent}-todo.json",
  146 + "shape": "const AgentTodolist = Type.Object({\n agent: Type.String(),\n call_id: Type.String(), // 与 AgentRuntimeState.call_id 关联\n created_at: Type.String(),\n items: Type.Array(Type.Object({\n id: Type.String({ pattern: '^step-\\\\d{3}$' }),\n description: Type.String(),\n status: Type.Union([Type.Literal('todo'), Type.Literal('doing'),\n Type.Literal('done'), Type.Literal('failed')]),\n started_at: Type.Optional(Type.String()),\n finished_at: Type.Optional(Type.String()),\n output_ref: Type.Optional(Type.String()), // 产物路径\n depends_on: Type.Optional(Type.Array(Type.String())),\n error: Type.Optional(Type.String()),\n })),\n current: Type.String(), // 当前 item.id\n progress_pct: Type.Number({ minimum:0, maximum:100 }),\n});\n\n// 典型 Architect generate 模式的 todolist 示例:\nitems: [\n { id:'step-001', description:'读 prd.html 并解析所有 section', status:'done' },\n { id:'step-002', description:'识别功能列表(共 5 个特性)', status:'done' },\n { id:'step-003', description:'拆分每个特性为 be/fe Unit', status:'doing' },\n { id:'step-004', description:'为每个 Unit 写 OpenAPI contract', status:'todo' },\n { id:'step-005', description:'生成 integration/spec.json', status:'todo' },\n]",
  147 + "contract": [
  148 + "Agent 在 system prompt 指引下,开始时必须先写完整 todolist(todo 状态)",
  149 + "每完成一项立即调 update_todo_item(id, status, output_ref) 工具",
  150 + "progress_pct = done 数 / total × 100",
  151 + "Dashboard 从这里渲染「Architect: 3/5 拆分 OAuth 功能为 3 个 Unit」",
  152 + "失败 → status=failed,触发 retry 时 todolist 清空重写"
  153 + ],
  154 + "tests": []
  155 + },
  156 + {
  157 + "name": "ProcessTimeline",
  158 + "path": "data/companies/{cid}/projects/{pid}/timeline.ndjson",
  159 + "shape": "// NDJSON,append-only,一行一事件,每行 ≤ 1KB\nconst TimelineEvent = Type.Object({\n ts: Type.Number(), // unix seconds\n source: Type.Union([\n Type.Literal('system'), // LoopController / DocStore / etc\n Type.Literal('human'), // 人类显式动作\n Type.Literal('pm-agent'),\n Type.Literal('architect'),\n Type.Literal('coder'),\n Type.Literal('tester'),\n Type.Literal('integrator'),\n Type.Literal('bug-triager'),\n ]),\n kind: Type.String(), // \"project_created\" / \"prd_drafted\" / etc\n ref: Type.Optional(Type.String()), // 关联资源,如 \"prd@rev3\"\n summary: Type.String({ maxLength: 280 }), // 人话摘要\n payload: Type.Optional(Type.Object({}, { additionalProperties:true })),\n});\n\n// 示例 (一行 = 一事件):\n{\"ts\":1747120731,\"source\":\"human\",\"kind\":\"project_created\",\"summary\":\"yibo 创建项目'做个 TODO API'\"}\n{\"ts\":1747120745,\"source\":\"pm-agent\",\"kind\":\"prd_drafted\",\"ref\":\"prd@rev1\",\"summary\":\"PRD rev 1 完成\"}\n{\"ts\":1747120798,\"source\":\"architect\",\"kind\":\"sdd_generated\",\"ref\":\"sdd@rev1\",\"summary\":\"拆 3 个 Unit\"}\n{\"ts\":1747120821,\"source\":\"coder\",\"kind\":\"unit_coded\",\"ref\":\"unit/todo-be\",\"summary\":\"todo-be 实现完成\"}\n{\"ts\":1747120833,\"source\":\"tester\",\"kind\":\"unit_failed\",\"ref\":\"unit/todo-be\",\"summary\":\"invalid-pwd 期望 401 收 500\"}",
  160 + "contract": [
  161 + "Append-only,永不修改已有行",
  162 + "所有 state 转移、文档变更、Agent 调用、Human 动作都必须 append 一条",
  163 + "是 audit log 也是 Dashboard 时间线视图数据源",
  164 + "每条 ≤ 1KB;超长内容(如完整 stderr)放 payload.{x}_ref 引用",
  165 + "由 timelineLogger.append(pid, event) 写入(C-15 工具)"
  166 + ],
  167 + "tests": []
  168 + },
  169 + {
  170 + "name": "BugsNdjson",
  171 + "path": "data/companies/{cid}/projects/{pid}/bugs.ndjson",
  172 + "shape": "// NDJSON,append + 状态机更新\nconst Bug = Type.Object({\n id: Type.String({ pattern: '^bug-\\\\d{8}-\\\\d{4}-[a-z0-9]{4}$' }),\n discovered_at:Type.String({ format: 'date-time' }),\n source: Type.Union([Type.Literal('unit_test'),\n Type.Literal('integration_test'),\n Type.Literal('triage'),\n Type.Literal('human')]),\n unit_id: Type.Optional(Type.String()),\n run_id: Type.Optional(Type.String()), // runs/{runId} 或 integration/runs/{runId}\n test_name: Type.Optional(Type.String()),\n category: Type.Union([Type.Literal('contract_mismatch'),\n Type.Literal('impl_bug'),\n Type.Literal('spec_ambiguous'),\n Type.Literal('infra_flake'),\n Type.Literal('unknown')]),\n severity: Type.Union([Type.Literal('critical'), Type.Literal('high'),\n Type.Literal('medium'), Type.Literal('low')]),\n title: Type.String({ maxLength: 120 }),\n detail: Type.String(), // 完整 stack/expected/got\n affected_units: Type.Array(Type.String()),\n status: Type.Union([Type.Literal('open'),\n Type.Literal('fixing'),\n Type.Literal('fixed'),\n Type.Literal('verified'),\n Type.Literal('wontfix'),\n Type.Literal('duplicate')]),\n status_history: Type.Array(Type.Object({\n ts: Type.String(),\n from: Type.String(), to: Type.String(),\n by: Type.String(), // agent/system/human id\n note: Type.Optional(Type.String()),\n })),\n fix_refs: Type.Array(Type.String()), // 关联的 run_id / triage / commit\n duplicate_of: Type.Optional(Type.String()), // 另一个 bug id\n});\n\n// 示例行:\n{\"id\":\"bug-20260513-0731-7f3a\",\"source\":\"unit_test\",\"unit_id\":\"auth-be\",\"run_id\":\"run-..0731\",\n \"category\":\"impl_bug\",\"severity\":\"high\",\"status\":\"fixed\",\n \"title\":\"login.invalid-pwd 期望 401 收 500\",\"affected_units\":[\"auth-be\"],\"fix_refs\":[\"run-..0734\"]}",
  173 + "contract": [
  174 + "每次 Tester / Integrator / BugTriager 失败 → 自动 append 1 条 bug,status=open",
  175 + "同一 unit 同一 test 连续两轮失败 → 视同同一 bug(合并),不重复 append",
  176 + "status 转换走 P-9 状态机,每次转换 append 一条 status_history",
  177 + "append-only:bug 内容修改通过新写一行 status=... 的更新事件实现(与 timeline 同理)",
  178 + "读取时取每个 bug.id 的最新一行作为当前状态"
  179 + ],
  180 + "tests": [
  181 + "单测红 → bugs.ndjson 新增 status=open 行",
  182 + "下一轮 Coder 修复 → 新增 status=fixed 行 with fix_refs=[新 run_id]",
  183 + "L2 通过 → 该 bug 状态最终 verified"
  184 + ]
  185 + },
  186 + {
  187 + "name": "ChangelogNdjson",
  188 + "path": "data/companies/{cid}/projects/{pid}/changelog.ndjson",
  189 + "shape": "const ChangelogEntry = Type.Object({\n ts: Type.String({ format: 'date-time' }),\n kind: Type.Union([\n Type.Literal('prd_rev'),\n Type.Literal('sdd_rev'),\n Type.Literal('units_rev'),\n Type.Literal('integration_spec_rev'),\n ]),\n rev: Type.Number(),\n by: Type.String(), // agent or human id\n trigger: Type.String(), // \"human_feedback\" | \"triage_patch\" | \"init\"\n summary: Type.String({ maxLength: 280 }),\n diff: Type.Object({ // 影响面快照\n sections_added: Type.Array(Type.String()),\n sections_changed: Type.Array(Type.String()),\n sections_removed: Type.Array(Type.String()),\n units_added: Type.Array(Type.String()),\n units_changed: Type.Array(Type.String()),\n units_removed: Type.Array(Type.String()),\n }),\n archive_ref: Type.String(), // history/prd-rev-{N}.html 路径\n});\n\n// 示例:\n{\"kind\":\"prd_rev\",\"rev\":3,\"by\":\"pm-agent\",\"trigger\":\"human_feedback\",\n \"summary\":\"+ OAuth 登录,影响 auth-mw 新增\",\"diff\":{\"sections_added\":[\"#oauth\"],...}}\n{\"kind\":\"sdd_rev\",\"rev\":5,\"by\":\"architect\",\"trigger\":\"prd@rev3\",\n \"summary\":\"3 个新 Unit: oauth-be / oauth-fe / auth-mw\",...}\n{\"kind\":\"sdd_rev\",\"rev\":6,\"by\":\"architect\",\"trigger\":\"triage_patch\",\n \"summary\":\"L2 修复:auth-be 401 明确\",...}",
  190 + "contract": [
  191 + "每次 DocStore.writePRD/writeSDD/writeUnits 成功后必 append 一行",
  192 + "diff 字段在 PM/Architect Agent 输出后由 DocStore 自动计算(cheerio + 拓扑 diff)",
  193 + "archive_ref 指向归档文件,可被 Dashboard 直接打开",
  194 + "这是 history/ 目录的线性视图:history/ 是物,changelog 是事"
  195 + ],
  196 + "tests": []
  197 + }
  198 + ],
  199 + "view": [
  200 + {
  201 + "name": "DevLoopDashboard",
  202 + "path": "web/src/views/DevLoopDashboard.vue",
  203 + "shape": "// Composition API · setup script\nconst props = defineProps<{}>(); // 无 props,从 route param 取 pid\n\nconst route = useRoute();\nconst pid = computed(() => route.params.pid as string);\nconst state = ref<ProjectState | null>(null);\nconst sse = useEventSource(`/api/devloop/projects/${pid.value}/stream`);\nconst modalOpen = ref(false);\nconst drawer = ref<Unit | null>(null);\n\n// 组件树:\n// DashboardHeader → LoopStatusBar → UnitsDAG → LiveAgentStream + PrdDiffPanel\n// 浮层:FeedbackModal · UnitDrawer",
  204 + "contract": [
  205 + "挂载 5 秒内必须发起 GET /state 请求",
  206 + "SSE 断线自动重连(指数退避,最长 30s)",
  207 + "页面卸载时关 SSE"
  208 + ],
  209 + "tests": []
  210 + },
  211 + {
  212 + "name": "DashboardHeader",
  213 + "path": "web/src/components/devloop/DashboardHeader.vue",
  214 + "shape": "defineProps<{\n project: { id: string, name: string, status: string }\n}>();\ndefineEmits<{\n feedback: [] // 点击 + NEW REQUIREMENT\n}>();\n\n// 渲染:\n// - 黑底标题栏\n// - 项目名(Fraunces 22px)\n// - 右上紫色按钮",
  215 + "contract": [],
  216 + "tests": []
  217 + },
  218 + {
  219 + "name": "LoopStatusBar",
  220 + "path": "web/src/components/devloop/LoopStatusBar.vue",
  221 + "shape": "defineProps<{\n l1: { passed: number, testing: number, pending: number, failed: number },\n l2: { lastRunAt?: string, status: 'idle'|'running'|'failed'|'shipped', rounds: number },\n l3: { rev: number, awaitingFeedback: boolean }\n}>();",
  222 + "contract": [
  223 + "三张卡片对应 emerald / amber / violet 三色,CSS 用 --L1/L2/L3 token",
  224 + "L2 running 时 amber 卡片有 pulse 动画",
  225 + "L3 awaitingFeedback=true 时显示 toast"
  226 + ],
  227 + "tests": []
  228 + },
  229 + {
  230 + "name": "UnitsDAG",
  231 + "path": "web/src/components/devloop/UnitsDAG.vue",
  232 + "shape": "defineProps<{\n units: Unit[]\n}>();\ndefineEmits<{\n select: [unit: Unit]\n}>();\n\n// 内部使用 SVG <rect> + <line>\n// > 50 unit 时切 d3-force 自动布局",
  233 + "contract": [
  234 + "节点配色由 unit.status 决定(见 ui-design-system 色板)",
  235 + "节点点击 emit select 事件,父组件打开 UnitDrawer",
  236 + "箭头方向 = depends_on (上游 → 下游)"
  237 + ],
  238 + "tests": []
  239 + },
  240 + {
  241 + "name": "LiveAgentStream",
  242 + "path": "web/src/components/devloop/LiveAgentStream.vue",
  243 + "shape": "defineProps<{\n events: AgentStreamEvent[] // 由父组件从 SSE 过滤 agent.stream 事件\n}>();\n\n// 渲染:\n// - 黑底 #0E0E10 终端样式\n// - 最近 500 行,老的丢\n// - PASS / FAIL 着色(绿/橙)\n// - 自动滚到底部,除非用户手动滚",
  244 + "contract": [],
  245 + "tests": []
  246 + },
  247 + {
  248 + "name": "PrdDiffPanel",
  249 + "path": "web/src/components/devloop/PrdDiffPanel.vue",
  250 + "shape": "defineProps<{\n prdHtml: string,\n prevRev: number | null\n}>();\n\n// 内部用 diff-dom 客户端算 section 级 diff\n// 新增 section 绿色 + 标记 / 删除红色删除线 / 修改黄色",
  251 + "contract": [],
  252 + "tests": []
  253 + },
  254 + {
  255 + "name": "FeedbackModal",
  256 + "path": "web/src/components/devloop/FeedbackModal.vue",
  257 + "shape": "defineProps<{ open: boolean }>();\ndefineEmits<{\n submit: [text: string],\n close: [],\n}>();\n\n// 单 textarea + 字数计数 + Ctrl+Enter 提交\n// 提交后立即 emit close,父组件负责发请求",
  258 + "contract": [],
  259 + "tests": [
  260 + "空文本 → 提交按钮 disabled",
  261 + "> 4000 字 → 警告但不阻止",
  262 + "Esc 触发 close"
  263 + ]
  264 + },
  265 + {
  266 + "name": "UnitDrawer",
  267 + "path": "web/src/components/devloop/UnitDrawer.vue",
  268 + "shape": "defineProps<{\n unit: Unit | null,\n runs: RunResult[] // 最近 3 次 run\n}>();\ndefineEmits<{ close: [] }>();\n\n// 右侧抽屉,从 right 滑入\n// 显示:spec.json 摘要 · contract.json · 最近 3 次 run 摘要 · 跳转完整日志按钮",
  269 + "contract": [],
  270 + "tests": []
  271 + },
  272 + {
  273 + "name": "Router",
  274 + "path": "web/src/router/index.js",
  275 + "shape": "// 在现有 routes 数组追加\n{\n path: '/devloop/:pid',\n name: 'DevLoop',\n component: () => import('@/views/DevLoopDashboard.vue'),\n meta: { requiresAuth: true },\n},\n{\n path: '/devloop', // 项目列表\n name: 'DevLoopList',\n component: () => import('@/views/DevLoopList.vue'),\n}",
  276 + "contract": [],
  277 + "tests": []
  278 + },
  279 + {
  280 + "name": "I18nKeys",
  281 + "path": "web/src/i18n/{zh-CN,en-US,...}/devloop.json",
  282 + "shape": "// 每个 locale 一份扁平 JSON:Record<string, string>\n{\n \"devloop.loop.l1\": \"...\",\n \"devloop.state.pending\": \"...\",\n \"devloop.btn.newRequirement\":\"...\"\n // ...完整 15 个 key 见下表\n}",
  283 + "contract": [],
  284 + "tests": []
  285 + },
  286 + {
  287 + "name": "DesignTokens",
  288 + "path": "web/src/styles/devloop.css",
  289 + "shape": ":root {\n --L1: #0E8C5A; --L1-tint: #E2F2EA;\n --L2: #C77C2C; --L2-tint: #F7E8D2;\n --L3: #6E48C3; --L3-tint: #ECE4FB;\n --alert: #C7341A;\n}\n// 与 LemonBoss 全局 token 共存,前缀避免冲突",
  290 + "contract": [],
  291 + "tests": []
  292 + }
  293 + ],
  294 + "control": [
  295 + {
  296 + "name": "REST.createProject",
  297 + "path": "server/routes/devloop.js",
  298 + "shape": "router.post('/projects', requireAuth, async (req, res) => {\n const { company_id, name, requirement } = req.body;\n const pid = await loopController.createProject({ company_id, name, owner: req.user.id });\n loopController.onHumanFeedback(pid, { text: requirement, origin: req.user.id });\n res.status(201).json({\n project_id: pid,\n status: 'prd_drafting',\n stream_url: `/api/devloop/projects/${pid}/stream`,\n });\n});",
  299 + "contract": [
  300 + "请求体 {company_id, name, requirement} 三字段必填",
  301 + "company_id 必须与 JWT 中的匹配(防越权)",
  302 + "立即返回 201 + project_id;PM Agent 异步起"
  303 + ],
  304 + "tests": [
  305 + "合法请求 → 201 + 含 project_id",
  306 + "company_id 不匹配 JWT → 403",
  307 + "缺字段 → 400 + 字段名"
  308 + ]
  309 + },
  310 + {
  311 + "name": "REST.postRequirement",
  312 + "path": "server/routes/devloop.js",
  313 + "shape": "router.post('/projects/:pid/requirement', requireAuth, async (req, res) => {\n const { text, origin } = req.body;\n loopController.onHumanFeedback(req.params.pid, { text, origin: origin || req.user.id });\n res.status(202).json({ accepted: true, queued: true });\n});",
  314 + "contract": [
  315 + "立即 202,不等 PM 跑完",
  316 + "多个反馈同时到 → 队列化,按时序处理",
  317 + "项目 status=shipped-final 或 abandoned → 拒绝(409)"
  318 + ],
  319 + "tests": []
  320 + },
  321 + {
  322 + "name": "REST.run",
  323 + "path": "server/routes/devloop.js",
  324 + "shape": "router.post('/projects/:pid/run', requireAuth, async (req, res) => {\n // 续跑(如果之前 blocked / paused,重启循环)\n loopController.runProject(req.params.pid);\n res.json({ running: true });\n});",
  325 + "contract": [
  326 + "等价于「resume」,对 status=running 是 no-op",
  327 + "blocked / l2_exhausted 状态调用此端点 → 重置预算并继续"
  328 + ],
  329 + "tests": []
  330 + },
  331 + {
  332 + "name": "REST.state",
  333 + "path": "server/routes/devloop.js",
  334 + "shape": "router.get('/projects/:pid/state', requireAuth, (req, res) => {\n res.json(loopController.getState(req.params.pid));\n});\n\n// 返回结构:\n{\n project_id, prd_rev, sdd_rev,\n current_loop: 'L1' | 'L2' | 'L3' | 'idle',\n units: Unit[],\n l2_rounds, l2_budget,\n agents_active: string[],\n token_used, token_budget,\n}",
  335 + "contract": [
  336 + "读时不加锁(不一致几毫秒可接受)",
  337 + "response 体限 100KB 以内(大项目 unit 多)"
  338 + ],
  339 + "tests": []
  340 + },
  341 + {
  342 + "name": "REST.stream",
  343 + "path": "server/routes/devloop.js",
  344 + "shape": "router.get('/projects/:pid/stream', requireAuth, (req, res) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n sseBroker.subscribe(req.params.pid, res, {\n replayFromOffset: req.query.last_event_id,\n });\n});",
  345 + "contract": [
  346 + "支持 Last-Event-ID 断线重连",
  347 + "心跳:每 25s 发一条 :keepalive 注释",
  348 + "项目结束(shipped/abandoned)→ 发 close 帧后关闭"
  349 + ],
  350 + "tests": []
  351 + },
  352 + {
  353 + "name": "REST.signOff",
  354 + "path": "server/routes/devloop.js",
  355 + "shape": "router.post('/projects/:pid/sign-off', requireAuth, (req, res) => {\n loopController.signOff(req.params.pid, req.user.id);\n res.json({ status: 'shipped-final' });\n});",
  356 + "contract": [],
  357 + "tests": []
  358 + },
  359 + {
  360 + "name": "REST.abandon",
  361 + "path": "server/routes/devloop.js",
  362 + "shape": "router.post('/projects/:pid/abandon', requireAuth, (req, res) => {\n loopController.abandon(req.params.pid, req.user.id);\n res.json({ status: 'abandoned' });\n});",
  363 + "contract": [],
  364 + "tests": []
  365 + },
  366 + {
  367 + "name": "LoopController",
  368 + "path": "server/src/core/loop-controller.js",
  369 + "shape": "module.exports.loopController = {\n createProject({ company_id, name, owner }): Promise<string>,\n\n runProject(pid: string): Promise<'shipped'|'blocked'|'l2_exhausted'>,\n runUnit(pid: string, unitId: string): Promise<UnitResult>,\n\n onHumanFeedback(pid: string, { text, origin }): void,\n onL2Failure(pid: string, runId: string): Promise<void>,\n\n signOff(pid: string, byUser: string): void,\n abandon(pid: string, byUser: string): void,\n\n getState(pid: string): ProjectState,\n};",
  370 + "contract": [
  371 + "所有 Agent 调用通过 agentManager.callAgent(name, input),不直接起进程",
  372 + "runProject 是幂等的:同一 pid 多次调用合并为一个执行",
  373 + "onHumanFeedback 同 pid 多次调用按时序队列处理"
  374 + ],
  375 + "tests": []
  376 + },
  377 + {
  378 + "name": "DocStore",
  379 + "path": "server/services/doc-store.js",
  380 + "shape": "module.exports.docStore = {\n // 读\n readPRD(pid): Promise<string>,\n readSDD(pid): Promise<string>,\n readUnits(pid): Promise<UnitsJson>,\n readIntegrationSpec(pid): Promise<IntegrationSpec>,\n\n // 写(自动归档)\n writePRD(pid, html): Promise<{ rev: number }>,\n writeSDD(pid, html): Promise<{ rev: number }>,\n writeUnits(pid, json): Promise<{ rev: number }>,\n writeIntegrationSpec(pid, json): Promise<void>,\n\n // 校验\n validateHTML(html, kind): { ok: boolean, errors: string[] },\n validateUnitsSchema(json): { ok: boolean, errors: string[] },\n\n // diff / 归档\n diffPRD(pid, fromRev, toRev): SectionDiff[],\n archive(pid, kind, oldContent): Promise<void>,\n\n // 工具\n hashContract(contractText): string, // → 'sha256:...'\n};",
  381 + "contract": [
  382 + "writeXxx 调用前先 validate;不通过抛 ValidationError",
  383 + "写入前自动归档旧版到 history/",
  384 + "所有方法可并发安全(用文件锁或队列)"
  385 + ],
  386 + "tests": []
  387 + },
  388 + {
  389 + "name": "UnitRegistry",
  390 + "path": "server/services/unit-registry.js",
  391 + "shape": "module.exports.unitRegistry = {\n list(pid): Unit[],\n pending(pid): Unit[],\n passed(pid): Unit[],\n blocked(pid): Unit[],\n anyBlocked(pid): boolean,\n\n l2GateOpen(pid): boolean,\n\n markCoding(pid, unitId): void,\n markTesting(pid, unitId): void,\n pass(pid, unitId, contractHash): void,\n fail(pid, unitId, reason): void,\n block(pid, unitId, reason): void,\n reset(pid, unitIds: string[]): void,\n\n diffWithSDD(pid): { added: string[], changed: string[], removed: string[] },\n};",
  392 + "contract": [
  393 + "所有 markXxx 内部走 FSM 校验(非法转换抛错)",
  394 + "每次状态变化同步发 SSE unit.state",
  395 + "状态来源单一:units.json + 内存缓存"
  396 + ],
  397 + "tests": []
  398 + },
  399 + {
  400 + "name": "SSEBroker",
  401 + "path": "server/services/sse-broker.js",
  402 + "shape": "module.exports.sseBroker = {\n subscribe(pid: string, res: ServerResponse, opts?: {\n replayFromOffset?: string\n }): void,\n\n emit(pid: string, event: string, data: any): void,\n\n close(pid: string): void, // 项目结束\n};",
  403 + "contract": [
  404 + "同一 pid 多订阅者广播",
  405 + "事件落盘到 stream.log,支持断线 replay",
  406 + "每 25s 心跳"
  407 + ],
  408 + "tests": []
  409 + },
  410 + {
  411 + "name": "Middleware",
  412 + "path": "server/middleware/",
  413 + "shape": "// 中间件标准签名 (Express 4)\ntype Middleware = (req: Request, res: Response, next: NextFunction) => void;\n// 4 个中间件:requireAuth · jsonSanitizer · jsonGuard(NEW) · tokenAccountant(NEW)",
  414 + "contract": [],
  415 + "tests": []
  416 + },
  417 + {
  418 + "name": "AgentCallConvention",
  419 + "path": "server/src/core/agent-manager.js · 复用既有,不新增文件",
  420 + "shape": "agentManager.callAgent(name: string, input: {\n pid: string, // 项目 id\n unit_id?: string, // Coder/Tester 时必填\n mode?: 'generate' | 'patch', // Architect 专属\n history?: any[], // retry 时上次失败\n ...其余 Agent 特定参数\n}): Promise<AgentResult>\n\n// AgentResult 必须含:\n{\n ok: boolean,\n outputs: string[], // 写入的文件路径\n duration_ms: number,\n tokens_used: number,\n error?: string,\n}",
  421 + "contract": [
  422 + "SessionPool 按 (name, cid, pid) 复用会话",
  423 + "每次调用先校验 name 在 _registry.json 中存在",
  424 + "失败时 stderr 自动落 stream.log"
  425 + ],
  426 + "tests": []
  427 + },
  428 + {
  429 + "name": "LoopController.getProgress",
  430 + "path": "server/src/core/loop-controller.js · 新方法",
  431 + "shape": "// 聚合进度 API,Dashboard 主用\ngetProgress(pid: string): {\n percent: number, // 0-100 整体加权进度\n current_phase: 'prd_drafting' | 'sdd_drafting' | 'L1' | 'L2' | 'shipped' | 'blocked',\n eta_seconds: number, // 估算剩余秒,基于历史数据\n breakdown: {\n prd: { ok: boolean, rev: number },\n sdd: { ok: boolean, rev: number },\n units: { passed: number, total: number, failed: number, blocked: number },\n l2: { rounds: number, budget: number },\n tokens: { used: number, budget: number },\n },\n active_agents: [{ // 当前正在跑的 Agent\n agent: string,\n unit_id?: string,\n step: string, // \"3/5\"\n desc: string, // 当前 todo item description\n started_at: string,\n }],\n recent_events: TimelineEvent[], // 最近 10 条\n}",
  432 + "contract": [
  433 + "聚合自 UnitsJson · agent-state/*.json · timeline.ndjson 三处",
  434 + "读时不加锁(毫秒级偏差可接受)",
  435 + "percent 加权算法:PRD(10) + SDD(20) + Units(50) + L2(15) + Ship(5) = 100",
  436 + "eta 基于历史项目均值 + 当前阶段剩余项数线性外推"
  437 + ],
  438 + "tests": [
  439 + "空项目 → percent=0, current_phase='prd_drafting'",
  440 + "所有 unit passed 但 L2 未跑 → percent ≈ 80",
  441 + "shipped → percent=100, eta_seconds=0"
  442 + ]
  443 + },
  444 + {
  445 + "name": "TimelineLogger",
  446 + "path": "server/services/timeline-logger.js",
  447 + "shape": "module.exports.timelineLogger = {\n append(pid: string, event: {\n source: string, kind: string,\n ref?: string, summary: string, payload?: object\n }): void,\n\n read(pid: string, opts?: {\n limit?: number, // 默认 100\n since?: number, // unix ts\n source?: string,\n kind?: string,\n }): TimelineEvent[],\n\n tail(pid: string, n: number = 10): TimelineEvent[],\n};",
  448 + "contract": [
  449 + "append 是同步操作(fs.appendFileSync),确保事件原子写入",
  450 + "ts 自动填当前时间,调用方不传",
  451 + "summary > 280 字符自动截断 + 省略号",
  452 + "payload 太大(> 4KB)→ 强制改为引用形式 { x_ref: \"...\" }",
  453 + "read 用流式 readline 解析,不一次性 load 全文件"
  454 + ],
  455 + "tests": []
  456 + },
  457 + {
  458 + "name": "BugTracker",
  459 + "path": "server/services/bug-tracker.js",
  460 + "shape": "module.exports.bugTracker = {\n // 写\n open(pid: string, bug: Omit<Bug, 'id'|'discovered_at'|'status'|'status_history'>): Bug,\n transition(pid: string, bugId: string, {\n to: BugStatus,\n by: string,\n note?: string,\n fix_refs?: string[],\n }): void,\n\n // 读 (流式 + 终态聚合)\n list(pid: string, filter?: {\n status?: BugStatus[],\n severity?: string[],\n unit_id?: string,\n since?: string,\n }): Bug[], // 返回每个 bug.id 的最新态\n\n history(pid: string, bugId: string): BugStatusHistory[],\n\n stats(pid: string): {\n open: number, fixing: number, fixed: number,\n verified: number, wontfix: number,\n by_severity: Record<string, number>,\n },\n\n // 工具\n dedupKey(bug: Partial<Bug>): string, // \"auth-be:login.invalid-pwd\" 用于检测重复\n};",
  461 + "contract": [
  462 + "open() 内部先算 dedupKey,命中已存在 open/fixing 的 bug → 不重复,返回旧 id",
  463 + "transition() 必经 P-9 状态机校验,非法转换抛错",
  464 + "list() 内部流式读 NDJSON(不一次性加载)",
  465 + "同时写入一条 timeline 事件 { kind: 'bug_opened'|'bug_transitioned', ref:bugId }",
  466 + "所有写操作同步发 SSE bug.* 事件"
  467 + ],
  468 + "tests": [
  469 + "同一 test 两次失败 → dedupKey 命中,只有一条 bug",
  470 + "transition open → verified(跳级)→ 抛错",
  471 + "stats() 与 list() 长度一致"
  472 + ]
  473 + },
  474 + {
  475 + "name": "ChangelogWriter",
  476 + "path": "server/services/changelog-writer.js",
  477 + "shape": "module.exports.changelogWriter = {\n append(pid: string, {\n kind, rev, by, trigger, summary, diff, archive_ref\n }): void,\n\n read(pid: string, opts?: { limit?, kind?, since? }): ChangelogEntry[],\n\n latest(pid: string, kind: string): ChangelogEntry | null,\n};\n\n// 被 DocStore 内部调用\n// DocStore.writePRD/writeSDD 成功后 → changelogWriter.append",
  478 + "contract": [
  479 + "由 DocStore 内部触发,外部不直接调",
  480 + "diff 字段计算由 DocStore 提供(cheerio 算 section 差异 + 拓扑算 units 差异)"
  481 + ],
  482 + "tests": []
  483 + }
  484 + ],
  485 + "process": [
  486 + {
  487 + "name": "Process.L1",
  488 + "path": "loop-controller.js → runUnit()",
  489 + "shape": "async function runUnit(pid, unitId) {\n const unit = unitRegistry.list(pid).find(u => u.id === unitId);\n let failures = [];\n\n for (let i = unit.retries; i < unit.retry_budget; i++) {\n unitRegistry.markCoding(pid, unitId);\n const coderRes = await agentManager.callAgent('coder', {\n pid, unit_id: unitId, history: failures\n });\n if (!coderRes.ok) { unitRegistry.fail(pid, unitId, 'coder_error'); continue; }\n\n unitRegistry.markTesting(pid, unitId);\n const testerRes = await agentManager.callAgent('tester', { pid, unit_id: unitId });\n if (testerRes.ok) {\n const hash = docStore.hashContract(await readContract(pid, unitId));\n unitRegistry.pass(pid, unitId, hash);\n return { ok: true, retries: i };\n }\n failures.push(testerRes.failures);\n unitRegistry.fail(pid, unitId, 'test_red');\n }\n\n unitRegistry.block(pid, unitId, 'budget_exceeded');\n return { ok: false };\n}",
  490 + "contract": [
  491 + "同一 Unit 任何时刻只允许一个 runUnit 实例(用 Map<unitId, Promise> 去重)",
  492 + "retry 之间不复用 Agent session(每次新 ACP session,保证干净)"
  493 + ],
  494 + "tests": []
  495 + },
  496 + {
  497 + "name": "Process.L2",
  498 + "path": "loop-controller.js → onL2Failure() · runProject() L2 段",
  499 + "shape": "function l2GateOpen(pid) {\n const units = unitRegistry.list(pid);\n const sdd = docStore.readSDD(pid);\n return units.every(u => {\n if (u.status !== 'passed') return false;\n const expectedHash = contractHashFromSDD(sdd, u.id);\n return u.contract_hash === expectedHash;\n });\n}",
  500 + "contract": [],
  501 + "tests": []
  502 + },
  503 + {
  504 + "name": "Process.L3",
  505 + "path": "loop-controller.js → onHumanFeedback()",
  506 + "shape": "// 每个项目维护一个反馈队列,串行消费\nconst feedbackQueues = new Map<string, Feedback[]>();\n\nfunction onHumanFeedback(pid, feedback) {\n if (!feedbackQueues.has(pid)) feedbackQueues.set(pid, []);\n feedbackQueues.get(pid).push(feedback);\n\n // 单 worker per pid\n if (!isWorking(pid)) drainFeedback(pid);\n}\n\nasync function drainFeedback(pid) {\n while (feedbackQueues.get(pid).length) {\n const fb = feedbackQueues.get(pid).shift();\n\n // 1. 等当前 L1 / L2 跑完(不打断)\n await waitForCurrentLoop(pid);\n\n // 2. PM 增量补 PRD\n await agentManager.callAgent('pm', { pid, feedback: fb });\n\n // 3. Architect 重算 SDD + units.json (mode=generate)\n await agentManager.callAgent('architect', { pid, mode: 'generate' });\n\n // 4. UnitRegistry.diffWithSDD → reset changed/removed\n const diff = unitRegistry.diffWithSDD(pid);\n unitRegistry.reset(pid, [...diff.changed, ...diff.removed]);\n\n // 5. 触发 runProject(增量 L1 + L2)\n runProject(pid);\n }\n}",
  507 + "contract": [
  508 + "同一 pid 永远只有一个 worker 在 drain(保证文档一致性)",
  509 + "多 pid 之间互不阻塞",
  510 + "worker 崩了,重启 server 后会自动 resume(从 stream.log 反推队列状态)"
  511 + ],
  512 + "tests": []
  513 + },
  514 + {
  515 + "name": "Process.AgentLifecycle",
  516 + "path": "server/src/core/session-pool.js · packages/acp-core/protocol/ClaudeCliProtocol.js · 复用既有",
  517 + "shape": "// 复用 LemonBoss SessionPool\n// key = (agentName, cid, pid)\n\n// 1. 首次 callAgent\n// ↓ SessionPool 检查无该 key\n// ↓ 通过 ProcessLauncher 起一个 claude CLI 子进程\n// ↓ 走 ClaudeCliProtocol.initialize() + newSession()\n// ↓ 注入 system prompt (system/templates/devloop-*.md)\n// ↓ sendPrompt(input)\n// ↓ 流式累积 → AgentResult\n\n// 2. 第二次同 key 的 callAgent\n// ↓ SessionPool 命中,复用 session\n// ↓ sendPrompt(input) ← 上下文延续\n\n// 3. 项目结束(shipped/abandoned)\n// ↓ SessionPool.closeProject(pid)\n// ↓ 杀掉所有该 pid 的 claude 进程\n\n// 4. 项目空闲 30 分钟\n// ↓ 自动 sweep 该 pid 的 session(省内存)\n// ↓ 下次 callAgent 重启冷启 OK",
  518 + "contract": [
  519 + "每个 Agent 进程绑死自己的 cwd(沙箱)",
  520 + "Bash 工具受 jsonGuard 中间件保护(防 inline JSON)",
  521 + "process-guard 看护:5s SIGTERM → 10s SIGKILL"
  522 + ],
  523 + "tests": []
  524 + },
  525 + {
  526 + "name": "Process.SSE",
  527 + "path": "server/services/sse-broker.js",
  528 + "shape": "// 每订阅一个 pid,open 一份连接列表\n// subscriptions: Map<pid, Set<ServerResponse>>\n\nfunction subscribe(pid, res, opts) {\n subscriptions.get(pid)?.add(res) || subscriptions.set(pid, new Set([res]));\n\n // Replay:从 stream.log 重放给当前订阅者\n if (opts?.replayFromOffset) replayTo(res, pid, opts.replayFromOffset);\n\n // 心跳\n const heartbeat = setInterval(() => res.write(':keepalive\\n\\n'), 25_000);\n res.on('close', () => { clearInterval(heartbeat); subscriptions.get(pid).delete(res); });\n}\n\nfunction emit(pid, event, data) {\n const id = ++eventCounter;\n const line = `id: ${id}\\nevent: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`;\n\n // 1. 落盘 stream.log\n fs.appendFileSync(streamLogPath(pid), line);\n // 2. 广播给所有订阅者\n for (const res of subscriptions.get(pid) || []) res.write(line);\n}",
  529 + "contract": [
  530 + "stream.log 保留 7 天,期间任何时刻 replay 都能精确",
  531 + "同事件不重复广播(去重以 event id 为准)"
  532 + ],
  533 + "tests": []
  534 + },
  535 + {
  536 + "name": "Process.Cleanup",
  537 + "path": "分布在 doc-store.js · unit-registry.js · sse-broker.js · scripts/devloop-cleanup-cron.js (NEW)",
  538 + "shape": "// 清理钩子标准签名(被各服务调用 + 一个日 cron)\ntype CleanupHook = (pid: string) => Promise<void>;\n// cron: scripts/devloop-cleanup-cron.js · 每日 04:00 跑",
  539 + "contract": [],
  540 + "tests": []
  541 + },
  542 + {
  543 + "name": "Process.Budget",
  544 + "path": "server/middleware/token-accountant.js (NEW) · 阈值嵌入 loop-controller.js",
  545 + "shape": "// 预算配置 schema(落在 project.json)\ntype ProjectBudgets = {\n retry_budget: number; // 默认 5 · 每 Unit\n l2_budget: number; // 默认 3 · 每 PRD rev\n token_budget: number; // 默认 5_000_000 · 项目\n agent_wallclock_ms:number; // 默认 600_000 · 单 Agent 调用\n integrator_wallclock_ms: number; // 默认 300_000 · 单 L2 run\n};",
  546 + "contract": [],
  547 + "tests": []
  548 + },
  549 + {
  550 + "name": "Process.StatePersistence",
  551 + "path": "loop-controller.js · agent-state/*.json · timeline.ndjson",
  552 + "shape": "// LoopController.callAgent 的完整包裹流程\nasync function callAgentWithPersistence(name, input) {\n const call_id = genCallId();\n const statePath = `${pidDir}/agent-state/${name}.json`;\n\n // ── 调用前 ──\n const inputSnap = await snapshot(input); // → agent-state/snapshots/{call_id}-in.json\n fs.writeFileSync(statePath, JSON.stringify({\n agent: name, call_id, status: 'running',\n current_step: 0, total_steps: 0, // Agent 自己稍后会写\n last_input_ref: inputSnap,\n started_at: now(), updated_at: now(),\n }));\n timelineLogger.append(pid, {\n source: 'system', kind: 'agent_started',\n ref: `agent/${name}/${call_id}`,\n summary: `${name} started (unit=${input.unit_id || '-'})`\n });\n\n try {\n // ── 调用中 ── (Agent 自己通过工具更新)\n // - record_memory(topic, content, relevance)\n // - write_todolist(items) → agent-state/{agent}-todo.json\n // - update_todo_item(id, status, ref) → 更新单条 item + 同步 progress_pct\n // - update_runtime_state(current_step) → 修 agent-state/{agent}.json\n // - timeline_append(kind, ref, summary)→ 写 timeline.ndjson\n const result = await agentManager.callAgent(name, input);\n\n // ── 调用后 (成功) ──\n const outSnap = await snapshot(result);\n fs.writeFileSync(statePath, JSON.stringify({\n ...readState(statePath),\n status: 'done',\n last_output_ref: outSnap,\n updated_at: now(),\n }));\n timelineLogger.append(pid, {\n source: name, kind: 'agent_done',\n ref: `agent/${name}/${call_id}`,\n summary: `${name} done in ${result.duration_ms}ms`,\n payload: { tokens: result.tokens_used }\n });\n return result;\n } catch (e) {\n // ── 调用后 (失败) ──\n fs.writeFileSync(statePath, JSON.stringify({\n ...readState(statePath),\n status: 'error',\n error: { code: e.code, message: e.message, stack: e.stack },\n updated_at: now(),\n }));\n timelineLogger.append(pid, {\n source: 'system', kind: 'agent_error',\n ref: `agent/${name}/${call_id}`,\n summary: e.message\n });\n throw e;\n }\n}",
  553 + "contract": [
  554 + "statePath 写入是原子的(fs.writeFileSync + sync flag)",
  555 + "resume_token 有效期 24 小时,过期视同 restart_fresh",
  556 + "memory 注入不超过 5 条,避免 prompt 膨胀",
  557 + "relevance < 0.5 的 memory 不注入但保留文件",
  558 + "同一 (pid, agent) 任意时刻只允许一个 status=running"
  559 + ],
  560 + "tests": []
  561 + },
  562 + {
  563 + "name": "Process.BugLifecycle",
  564 + "path": "server/services/bug-tracker.js → transition()",
  565 + "shape": "// L1 内:Tester 失败 → 开 bug + 设 unit failed\ntesterResult.failures.forEach(f => {\n bugTracker.open(pid, {\n source: 'unit_test', unit_id: unit.id, run_id: runId,\n test_name: f.test, category: 'unknown',\n severity: 'high', title: f.test + ' ' + brief(f),\n detail: JSON.stringify(f), affected_units: [unit.id]\n });\n});\n\n// 下一轮 Coder 进入 → 关联 bug 状态转 fixing\nbugTracker.list(pid, { unit_id: unit.id, status: ['open'] })\n .forEach(b => bugTracker.transition(pid, b.id, { to:'fixing', by:'coder' }));\n\n// L2 通过 → 所有 fixed → verified\nif (integratorResult.ok) {\n bugTracker.list(pid, { status: ['fixed'] })\n .forEach(b => bugTracker.transition(pid, b.id, { to:'verified', by:'system' }));\n}",
  566 + "contract": [],
  567 + "tests": []
  568 + }
  569 + ]
  570 +}
0 571 \ No newline at end of file
... ...