From 53eda8d2d677cfd49700a38579286f366fc7363d Mon Sep 17 00:00:00 2001 From: yibo Date: Wed, 13 May 2026 12:23:04 +0800 Subject: [PATCH] chore(prd): snapshot build-manifest.json (52 MVCP defs, 100% extraction) --- build-manifest.json | 570 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 570 insertions(+), 0 deletions(-) create mode 100644 build-manifest.json diff --git a/build-manifest.json b/build-manifest.json new file mode 100644 index 0000000..854bd92 --- /dev/null +++ b/build-manifest.json @@ -0,0 +1,570 @@ +{ + "meta": { + "source": "docs/prd/newfunction", + "prdRev": "v0.2", + "extractedAt": "2026-05-13T04:16:35.145Z" + }, + "model": [ + { + "name": "ProjectMeta", + "path": "data/companies/{cid}/projects/{pid}/project.json", + "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});", + "contract": [ + "id 全局唯一,跨 company 不重复", + "status 只能按状态机合法转换(见 sdd-process.html · M-process-project)", + "token_used > token_budget 时 LoopController 自动挂起项目" + ], + "tests": [] + }, + { + "name": "UnitsJson", + "path": "data/companies/{cid}/projects/{pid}/units.json", + "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});", + "contract": [ + "DAG 必须无环(DocStore 写入前用拓扑排序校验)", + "depends_on 引用的 id 必须存在于本文件 units 中", + "contract_hash = sha256(SDD 中对应 section
 文本) 截前 16 位"
+      ],
+      "tests": [
+        "循环依赖 → 拒绝写入",
+        "未知 id → 拒绝写入",
+        "所有 status 为 passed 且 hash 与当前 SDD 匹配 → l2GateOpen 返回 true"
+      ]
+    },
+    {
+      "name": "PRD-Site",
+      "path": "data/companies/{cid}/projects/{pid}/prd/ (目录)",
+      "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
\n
\n

...

\n

...

\n
\n // 可选,index.html only\n
...
\n
", + "contract": [ + "顶级必须是
,每页都有", + "每个 section 必含

+ 至少一个

", + "section data-rev 单调递增(页内),article data-rev 取该页内最大", + "每页至少覆盖 4/8 类 HTML 信息(见 agents-pm §2.2)", + "data-conflict aside 仅出现在 index.html 顶部,存在时整个项目暂停等人", + "11 个必备章节缺一个 → validate_prd_site 失败" + ], + "tests": [ + "init 模式后 11 章节全在", + "incremental 模式后仅 affected 章节被改", + "多 rev 后 history/ 含旧版归档" + ] + }, + { + "name": "SDD-HTML", + "path": "data/companies/{cid}/projects/{pid}/sdd.html", + "shape": "

\n \n

Unit · auth-be

\n

...功能描述...

\n
\n      POST /api/auth/login ...\n    
\n
    \n
  • acceptance criteria 1
  • \n
\n \n
", + "contract": [ + "每个 unit section 必含 contract + acceptance", + "data-depends 空格分隔 unit id 列表,可为空", + "data-derived-from 指向产生它的 prd 版本" + ], + "tests": [] + }, + { + "name": "ContractJson", + "path": ".../units/{unitId}/contract.json", + "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});", + "contract": [], + "tests": [] + }, + { + "name": "IntegrationSpec", + "path": ".../integration/spec.json", + "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});", + "contract": [ + "involves 列表必须是 cases.steps.unit 的去重并集,BugTriager 用它做归因粗筛", + "headers / body 中可用 {{stepN.field}} 占位符引用前面步骤结果" + ], + "tests": [] + }, + { + "name": "RunResult", + "path": ".../units/{unitId}/runs/{runId}/result.json", + "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});", + "contract": [], + "tests": [] + }, + { + "name": "IntegrationResult", + "path": "data/companies/{cid}/projects/{pid}/integration/runs/{runId}/result.json", + "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});", + "contract": [], + "tests": [] + }, + { + "name": "TriageJson", + "path": ".../integration/runs/{runId}/triage.json", + "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});", + "contract": [ + "category = contract_mismatch → proposed_sdd_patch 必填", + "category = impl_bug → proposed_sdd_patch 必为 null(不改 SDD,只 reset Unit)", + "confidence < 0.5 → LoopController 上报 L3 不自动应用", + "每条 evidence 必须在原文件中能 grep 到" + ], + "tests": [] + }, + { + "name": "AgentRuntimeState", + "path": "data/companies/{cid}/projects/{pid}/agent-state/{agent}.json", + "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});", + "contract": [ + "每次 LoopController.callAgent 前写一次(status=running)", + "Agent 自己在跨越主要 checkpoint 时通过 update_runtime_state 工具更新 current_step", + "调用结束(成功或失败)由 LoopController 写终态 status=done|error", + "Server SIGTERM 时所有 status=running 自动改 status=paused 并写 resume_token", + "Server 启动时扫所有 paused,发 SSE resume_decision_needed 等人决策" + ], + "tests": [ + "Agent 崩溃 → state.json status=error,error 字段有 code/message", + "Server kill -9 → 重启后 state.json status=paused,resume_token 非空", + "Dashboard 能从 state.json 渲染「Architect at step 3/5」" + ] + }, + { + "name": "AgentMemory", + "path": "data/companies/{cid}/memory/devloop/{agent}/{topic}.md", + "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", + "contract": [ + "每个 Agent 启动时扫 memory/devloop/{agent}/*.md", + "按 relevance_score × recency 排序,取前 5 条注入 system prompt", + "Agent 自己可通过 record_memory(topic, content, relevance) 工具新增", + "Dedup:同 agent + 同 topic 是 upsert(覆盖 + 累加 hits),不新建文件。文件名 = {topic-slug}.md,topic-slug 由 record_memory 工具按规则计算(小写 · 空格转 -)", + "memory 跨项目(同 company 内)共享,不跨 company", + "frontmatter 必须含 agent / topic / last_updated 三字段", + "单文件 ≤ 8KB,超出 record_memory 自动 truncate 旧内容(保留新加 + 最相关)", + "v1 注意:如果初版 token 紧张可以关 memory 注入(top-5 改 top-0),完全跳过。memory 在 ≥ 5 个项目后才开始有积累价值。" + ], + "tests": [ + "新 Agent 调用 → memory dir 存在则 system prompt 含 \"## Memory\" section", + "不存在该 agent dir → fallback 空 memory,不报错", + "record_memory 写入后下次启动能读到" + ] + }, + { + "name": "AgentTodolist", + "path": "data/companies/{cid}/projects/{pid}/agent-state/{agent}-todo.json", + "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]", + "contract": [ + "Agent 在 system prompt 指引下,开始时必须先写完整 todolist(todo 状态)", + "每完成一项立即调 update_todo_item(id, status, output_ref) 工具", + "progress_pct = done 数 / total × 100", + "Dashboard 从这里渲染「Architect: 3/5 拆分 OAuth 功能为 3 个 Unit」", + "失败 → status=failed,触发 retry 时 todolist 清空重写" + ], + "tests": [] + }, + { + "name": "ProcessTimeline", + "path": "data/companies/{cid}/projects/{pid}/timeline.ndjson", + "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\"}", + "contract": [ + "Append-only,永不修改已有行", + "所有 state 转移、文档变更、Agent 调用、Human 动作都必须 append 一条", + "是 audit log 也是 Dashboard 时间线视图数据源", + "每条 ≤ 1KB;超长内容(如完整 stderr)放 payload.{x}_ref 引用", + "由 timelineLogger.append(pid, event) 写入(C-15 工具)" + ], + "tests": [] + }, + { + "name": "BugsNdjson", + "path": "data/companies/{cid}/projects/{pid}/bugs.ndjson", + "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\"]}", + "contract": [ + "每次 Tester / Integrator / BugTriager 失败 → 自动 append 1 条 bug,status=open", + "同一 unit 同一 test 连续两轮失败 → 视同同一 bug(合并),不重复 append", + "status 转换走 P-9 状态机,每次转换 append 一条 status_history", + "append-only:bug 内容修改通过新写一行 status=... 的更新事件实现(与 timeline 同理)", + "读取时取每个 bug.id 的最新一行作为当前状态" + ], + "tests": [ + "单测红 → bugs.ndjson 新增 status=open 行", + "下一轮 Coder 修复 → 新增 status=fixed 行 with fix_refs=[新 run_id]", + "L2 通过 → 该 bug 状态最终 verified" + ] + }, + { + "name": "ChangelogNdjson", + "path": "data/companies/{cid}/projects/{pid}/changelog.ndjson", + "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 明确\",...}", + "contract": [ + "每次 DocStore.writePRD/writeSDD/writeUnits 成功后必 append 一行", + "diff 字段在 PM/Architect Agent 输出后由 DocStore 自动计算(cheerio + 拓扑 diff)", + "archive_ref 指向归档文件,可被 Dashboard 直接打开", + "这是 history/ 目录的线性视图:history/ 是物,changelog 是事" + ], + "tests": [] + } + ], + "view": [ + { + "name": "DevLoopDashboard", + "path": "web/src/views/DevLoopDashboard.vue", + "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(null);\nconst sse = useEventSource(`/api/devloop/projects/${pid.value}/stream`);\nconst modalOpen = ref(false);\nconst drawer = ref(null);\n\n// 组件树:\n// DashboardHeader → LoopStatusBar → UnitsDAG → LiveAgentStream + PrdDiffPanel\n// 浮层:FeedbackModal · UnitDrawer", + "contract": [ + "挂载 5 秒内必须发起 GET /state 请求", + "SSE 断线自动重连(指数退避,最长 30s)", + "页面卸载时关 SSE" + ], + "tests": [] + }, + { + "name": "DashboardHeader", + "path": "web/src/components/devloop/DashboardHeader.vue", + "shape": "defineProps<{\n project: { id: string, name: string, status: string }\n}>();\ndefineEmits<{\n feedback: [] // 点击 + NEW REQUIREMENT\n}>();\n\n// 渲染:\n// - 黑底标题栏\n// - 项目名(Fraunces 22px)\n// - 右上紫色按钮", + "contract": [], + "tests": [] + }, + { + "name": "LoopStatusBar", + "path": "web/src/components/devloop/LoopStatusBar.vue", + "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}>();", + "contract": [ + "三张卡片对应 emerald / amber / violet 三色,CSS 用 --L1/L2/L3 token", + "L2 running 时 amber 卡片有 pulse 动画", + "L3 awaitingFeedback=true 时显示 toast" + ], + "tests": [] + }, + { + "name": "UnitsDAG", + "path": "web/src/components/devloop/UnitsDAG.vue", + "shape": "defineProps<{\n units: Unit[]\n}>();\ndefineEmits<{\n select: [unit: Unit]\n}>();\n\n// 内部使用 SVG + \n// > 50 unit 时切 d3-force 自动布局", + "contract": [ + "节点配色由 unit.status 决定(见 ui-design-system 色板)", + "节点点击 emit select 事件,父组件打开 UnitDrawer", + "箭头方向 = depends_on (上游 → 下游)" + ], + "tests": [] + }, + { + "name": "LiveAgentStream", + "path": "web/src/components/devloop/LiveAgentStream.vue", + "shape": "defineProps<{\n events: AgentStreamEvent[] // 由父组件从 SSE 过滤 agent.stream 事件\n}>();\n\n// 渲染:\n// - 黑底 #0E0E10 终端样式\n// - 最近 500 行,老的丢\n// - PASS / FAIL 着色(绿/橙)\n// - 自动滚到底部,除非用户手动滚", + "contract": [], + "tests": [] + }, + { + "name": "PrdDiffPanel", + "path": "web/src/components/devloop/PrdDiffPanel.vue", + "shape": "defineProps<{\n prdHtml: string,\n prevRev: number | null\n}>();\n\n// 内部用 diff-dom 客户端算 section 级 diff\n// 新增 section 绿色 + 标记 / 删除红色删除线 / 修改黄色", + "contract": [], + "tests": [] + }, + { + "name": "FeedbackModal", + "path": "web/src/components/devloop/FeedbackModal.vue", + "shape": "defineProps<{ open: boolean }>();\ndefineEmits<{\n submit: [text: string],\n close: [],\n}>();\n\n// 单 textarea + 字数计数 + Ctrl+Enter 提交\n// 提交后立即 emit close,父组件负责发请求", + "contract": [], + "tests": [ + "空文本 → 提交按钮 disabled", + "> 4000 字 → 警告但不阻止", + "Esc 触发 close" + ] + }, + { + "name": "UnitDrawer", + "path": "web/src/components/devloop/UnitDrawer.vue", + "shape": "defineProps<{\n unit: Unit | null,\n runs: RunResult[] // 最近 3 次 run\n}>();\ndefineEmits<{ close: [] }>();\n\n// 右侧抽屉,从 right 滑入\n// 显示:spec.json 摘要 · contract.json · 最近 3 次 run 摘要 · 跳转完整日志按钮", + "contract": [], + "tests": [] + }, + { + "name": "Router", + "path": "web/src/router/index.js", + "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}", + "contract": [], + "tests": [] + }, + { + "name": "I18nKeys", + "path": "web/src/i18n/{zh-CN,en-US,...}/devloop.json", + "shape": "// 每个 locale 一份扁平 JSON:Record\n{\n \"devloop.loop.l1\": \"...\",\n \"devloop.state.pending\": \"...\",\n \"devloop.btn.newRequirement\":\"...\"\n // ...完整 15 个 key 见下表\n}", + "contract": [], + "tests": [] + }, + { + "name": "DesignTokens", + "path": "web/src/styles/devloop.css", + "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 共存,前缀避免冲突", + "contract": [], + "tests": [] + } + ], + "control": [ + { + "name": "REST.createProject", + "path": "server/routes/devloop.js", + "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});", + "contract": [ + "请求体 {company_id, name, requirement} 三字段必填", + "company_id 必须与 JWT 中的匹配(防越权)", + "立即返回 201 + project_id;PM Agent 异步起" + ], + "tests": [ + "合法请求 → 201 + 含 project_id", + "company_id 不匹配 JWT → 403", + "缺字段 → 400 + 字段名" + ] + }, + { + "name": "REST.postRequirement", + "path": "server/routes/devloop.js", + "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});", + "contract": [ + "立即 202,不等 PM 跑完", + "多个反馈同时到 → 队列化,按时序处理", + "项目 status=shipped-final 或 abandoned → 拒绝(409)" + ], + "tests": [] + }, + { + "name": "REST.run", + "path": "server/routes/devloop.js", + "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});", + "contract": [ + "等价于「resume」,对 status=running 是 no-op", + "blocked / l2_exhausted 状态调用此端点 → 重置预算并继续" + ], + "tests": [] + }, + { + "name": "REST.state", + "path": "server/routes/devloop.js", + "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}", + "contract": [ + "读时不加锁(不一致几毫秒可接受)", + "response 体限 100KB 以内(大项目 unit 多)" + ], + "tests": [] + }, + { + "name": "REST.stream", + "path": "server/routes/devloop.js", + "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});", + "contract": [ + "支持 Last-Event-ID 断线重连", + "心跳:每 25s 发一条 :keepalive 注释", + "项目结束(shipped/abandoned)→ 发 close 帧后关闭" + ], + "tests": [] + }, + { + "name": "REST.signOff", + "path": "server/routes/devloop.js", + "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});", + "contract": [], + "tests": [] + }, + { + "name": "REST.abandon", + "path": "server/routes/devloop.js", + "shape": "router.post('/projects/:pid/abandon', requireAuth, (req, res) => {\n loopController.abandon(req.params.pid, req.user.id);\n res.json({ status: 'abandoned' });\n});", + "contract": [], + "tests": [] + }, + { + "name": "LoopController", + "path": "server/src/core/loop-controller.js", + "shape": "module.exports.loopController = {\n createProject({ company_id, name, owner }): Promise,\n\n runProject(pid: string): Promise<'shipped'|'blocked'|'l2_exhausted'>,\n runUnit(pid: string, unitId: string): Promise,\n\n onHumanFeedback(pid: string, { text, origin }): void,\n onL2Failure(pid: string, runId: string): Promise,\n\n signOff(pid: string, byUser: string): void,\n abandon(pid: string, byUser: string): void,\n\n getState(pid: string): ProjectState,\n};", + "contract": [ + "所有 Agent 调用通过 agentManager.callAgent(name, input),不直接起进程", + "runProject 是幂等的:同一 pid 多次调用合并为一个执行", + "onHumanFeedback 同 pid 多次调用按时序队列处理" + ], + "tests": [] + }, + { + "name": "DocStore", + "path": "server/services/doc-store.js", + "shape": "module.exports.docStore = {\n // 读\n readPRD(pid): Promise,\n readSDD(pid): Promise,\n readUnits(pid): Promise,\n readIntegrationSpec(pid): Promise,\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,\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,\n\n // 工具\n hashContract(contractText): string, // → 'sha256:...'\n};", + "contract": [ + "writeXxx 调用前先 validate;不通过抛 ValidationError", + "写入前自动归档旧版到 history/", + "所有方法可并发安全(用文件锁或队列)" + ], + "tests": [] + }, + { + "name": "UnitRegistry", + "path": "server/services/unit-registry.js", + "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};", + "contract": [ + "所有 markXxx 内部走 FSM 校验(非法转换抛错)", + "每次状态变化同步发 SSE unit.state", + "状态来源单一:units.json + 内存缓存" + ], + "tests": [] + }, + { + "name": "SSEBroker", + "path": "server/services/sse-broker.js", + "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};", + "contract": [ + "同一 pid 多订阅者广播", + "事件落盘到 stream.log,支持断线 replay", + "每 25s 心跳" + ], + "tests": [] + }, + { + "name": "Middleware", + "path": "server/middleware/", + "shape": "// 中间件标准签名 (Express 4)\ntype Middleware = (req: Request, res: Response, next: NextFunction) => void;\n// 4 个中间件:requireAuth · jsonSanitizer · jsonGuard(NEW) · tokenAccountant(NEW)", + "contract": [], + "tests": [] + }, + { + "name": "AgentCallConvention", + "path": "server/src/core/agent-manager.js · 复用既有,不新增文件", + "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\n\n// AgentResult 必须含:\n{\n ok: boolean,\n outputs: string[], // 写入的文件路径\n duration_ms: number,\n tokens_used: number,\n error?: string,\n}", + "contract": [ + "SessionPool 按 (name, cid, pid) 复用会话", + "每次调用先校验 name 在 _registry.json 中存在", + "失败时 stderr 自动落 stream.log" + ], + "tests": [] + }, + { + "name": "LoopController.getProgress", + "path": "server/src/core/loop-controller.js · 新方法", + "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}", + "contract": [ + "聚合自 UnitsJson · agent-state/*.json · timeline.ndjson 三处", + "读时不加锁(毫秒级偏差可接受)", + "percent 加权算法:PRD(10) + SDD(20) + Units(50) + L2(15) + Ship(5) = 100", + "eta 基于历史项目均值 + 当前阶段剩余项数线性外推" + ], + "tests": [ + "空项目 → percent=0, current_phase='prd_drafting'", + "所有 unit passed 但 L2 未跑 → percent ≈ 80", + "shipped → percent=100, eta_seconds=0" + ] + }, + { + "name": "TimelineLogger", + "path": "server/services/timeline-logger.js", + "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};", + "contract": [ + "append 是同步操作(fs.appendFileSync),确保事件原子写入", + "ts 自动填当前时间,调用方不传", + "summary > 280 字符自动截断 + 省略号", + "payload 太大(> 4KB)→ 强制改为引用形式 { x_ref: \"...\" }", + "read 用流式 readline 解析,不一次性 load 全文件" + ], + "tests": [] + }, + { + "name": "BugTracker", + "path": "server/services/bug-tracker.js", + "shape": "module.exports.bugTracker = {\n // 写\n open(pid: string, bug: Omit): 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,\n },\n\n // 工具\n dedupKey(bug: Partial): string, // \"auth-be:login.invalid-pwd\" 用于检测重复\n};", + "contract": [ + "open() 内部先算 dedupKey,命中已存在 open/fixing 的 bug → 不重复,返回旧 id", + "transition() 必经 P-9 状态机校验,非法转换抛错", + "list() 内部流式读 NDJSON(不一次性加载)", + "同时写入一条 timeline 事件 { kind: 'bug_opened'|'bug_transitioned', ref:bugId }", + "所有写操作同步发 SSE bug.* 事件" + ], + "tests": [ + "同一 test 两次失败 → dedupKey 命中,只有一条 bug", + "transition open → verified(跳级)→ 抛错", + "stats() 与 list() 长度一致" + ] + }, + { + "name": "ChangelogWriter", + "path": "server/services/changelog-writer.js", + "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", + "contract": [ + "由 DocStore 内部触发,外部不直接调", + "diff 字段计算由 DocStore 提供(cheerio 算 section 差异 + 拓扑算 units 差异)" + ], + "tests": [] + } + ], + "process": [ + { + "name": "Process.L1", + "path": "loop-controller.js → runUnit()", + "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}", + "contract": [ + "同一 Unit 任何时刻只允许一个 runUnit 实例(用 Map 去重)", + "retry 之间不复用 Agent session(每次新 ACP session,保证干净)" + ], + "tests": [] + }, + { + "name": "Process.L2", + "path": "loop-controller.js → onL2Failure() · runProject() L2 段", + "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}", + "contract": [], + "tests": [] + }, + { + "name": "Process.L3", + "path": "loop-controller.js → onHumanFeedback()", + "shape": "// 每个项目维护一个反馈队列,串行消费\nconst feedbackQueues = new Map();\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}", + "contract": [ + "同一 pid 永远只有一个 worker 在 drain(保证文档一致性)", + "多 pid 之间互不阻塞", + "worker 崩了,重启 server 后会自动 resume(从 stream.log 反推队列状态)" + ], + "tests": [] + }, + { + "name": "Process.AgentLifecycle", + "path": "server/src/core/session-pool.js · packages/acp-core/protocol/ClaudeCliProtocol.js · 复用既有", + "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", + "contract": [ + "每个 Agent 进程绑死自己的 cwd(沙箱)", + "Bash 工具受 jsonGuard 中间件保护(防 inline JSON)", + "process-guard 看护:5s SIGTERM → 10s SIGKILL" + ], + "tests": [] + }, + { + "name": "Process.SSE", + "path": "server/services/sse-broker.js", + "shape": "// 每订阅一个 pid,open 一份连接列表\n// subscriptions: Map>\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}", + "contract": [ + "stream.log 保留 7 天,期间任何时刻 replay 都能精确", + "同事件不重复广播(去重以 event id 为准)" + ], + "tests": [] + }, + { + "name": "Process.Cleanup", + "path": "分布在 doc-store.js · unit-registry.js · sse-broker.js · scripts/devloop-cleanup-cron.js (NEW)", + "shape": "// 清理钩子标准签名(被各服务调用 + 一个日 cron)\ntype CleanupHook = (pid: string) => Promise;\n// cron: scripts/devloop-cleanup-cron.js · 每日 04:00 跑", + "contract": [], + "tests": [] + }, + { + "name": "Process.Budget", + "path": "server/middleware/token-accountant.js (NEW) · 阈值嵌入 loop-controller.js", + "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};", + "contract": [], + "tests": [] + }, + { + "name": "Process.StatePersistence", + "path": "loop-controller.js · agent-state/*.json · timeline.ndjson", + "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}", + "contract": [ + "statePath 写入是原子的(fs.writeFileSync + sync flag)", + "resume_token 有效期 24 小时,过期视同 restart_fresh", + "memory 注入不超过 5 条,避免 prompt 膨胀", + "relevance < 0.5 的 memory 不注入但保留文件", + "同一 (pid, agent) 任意时刻只允许一个 status=running" + ], + "tests": [] + }, + { + "name": "Process.BugLifecycle", + "path": "server/services/bug-tracker.js → transition()", + "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}", + "contract": [], + "tests": [] + } + ] +} \ No newline at end of file -- libgit2 0.21.4