{ "meta": { "source": "docs/prd/newfunction", "prdRev": "v0.2", "extractedAt": "2026-08-18T07:53:53.175Z" }, "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\n // 项目根 (见 arch-project-root)\n mode: Type.Union([\n Type.Literal('sandbox'), // 默认,根在 data/.../projects/{pid}/\n Type.Literal('linked-new'), // 用户指定空目录\n Type.Literal('linked-existing'), // v2:复用已有 git repo\n ]),\n root_path: Type.String(), // 绝对路径,所有派生路径的源\n\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 {\n company_id, name, requirement,\n mode = 'sandbox', // 默认 sandbox\n target_path // linked-* 时必填\n } = req.body;\n\n const { pid, root_path } = await loopController.createProject({\n company_id, name, owner: req.user.id,\n mode, target_path,\n });\n loopController.onHumanFeedback(pid, { text: requirement, origin: req.user.id });\n res.status(201).json({\n project_id: pid,\n mode, root_path,\n status: 'prd_drafting',\n stream_url: `/api/devloop/projects/${pid}/stream`,\n });\n});", "contract": [ "请求体必填:{company_id, name, requirement};可选:{mode, target_path}", "mode 默认 sandbox;linked-new 时 target_path 必填且必须是绝对空目录或不存在;linked-existing v1 拒收(v2 启用)", "target_path 不允许指向系统敏感目录(详见 arch-project-root §⑫)", "company_id 必须与 JWT 中的匹配(防越权)", "立即返回 201 + project_id + root_path;PM Agent 异步起" ], "tests": [ "合法 sandbox 请求 → 201 + 含 project_id + root_path=data/.../projects/{pid}/", "linked-new + target_path 合法 → 201 + root_path=用户指定路径 + .git 已初始化", "linked-new + target_path 不存在父目录 → 400 拒绝", "linked-new + target_path 是 /etc → 403 sensitive_path", "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": [] }, { "name": "GitVault", "path": "server/services/git-vault.js", "shape": "// 详尽设计见 arch-git-versioning.html\nmodule.exports.gitVault = {\n init(pid, opts?): Promise<{ ok, head }>,\n commit(pid, { kind, unit_id?, run_id?, summary, files?, author?, skipEmpty? }):\n Promise<{ ok, sha?, skipped? }>,\n tag(pid, tagName, opts?): Promise<{ ok, tag }>,\n log(pid, opts?: { limit?, since?, grep? }): Promise,\n diff(pid, refA, refB, opts?): Promise,\n show(pid, sha): Promise,\n listTags(pid): Promise,\n status(pid): Promise<{ dirty, files }>,\n resetWorkingTree(pid, ref = 'HEAD'): Promise<{ ok }>,\n revert(pid, sha): Promise<{ ok, newSha }>,\n archive(pid, ref = 'HEAD', format = 'zip'): Promise<{ path }>,\n // v2 留口\n addRemote?(pid, name, url),\n push?(pid, remote),\n};", "contract": [ "实现用 child_process.spawn('git', ...),不依赖第三方 npm 包(避免凭据管理复杂化)", "所有方法按 pid 串行化(per-pid mutex),避免同项目并发 git 操作打架", "commit author 标 Agent 名(e.g. \"PM Agent \"),git log 可读「哪个 Agent 干的」", "nothing-to-commit 不抛错,返 { ok: true, skipped: true }", "commit 失败不阻塞 Agent 返回(git 是 nice-to-have,不是阻塞依赖)" ], "tests": [ "init 后 .git 存在,.gitignore 已 commit", "commit({kind:'prd'}) 后 git log 有该 commit,author 是 \"PM Agent\"", "nothing-to-commit 返 skipped=true 不抛", "tag('shipped-v1') 后 listTags 包含 shipped-v1", "archive 返回 zip 路径,unzip -l 看仅 tracked files" ] } ], "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/agents/devloop-*/prompt.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": [] }, { "name": "Process.GitCommit", "path": "server/src/core/loop-controller.js · callAgentWithPersistence 的 success 分支扩展", "shape": "// 完整设计见 arch-git-versioning.html · 此处只放骨架\n\nasync function commitAfterAgent(pid, name, result, input) {\n const spec = buildCommitSpec(name, result, input);\n if (!spec) return; // 某些情况不 commit (Coder 等 Tester 绿,Tester 红不 commit)\n\n const { ok, sha, skipped } = await gitVault.commit(pid, spec);\n\n if (ok && !skipped) {\n timelineLogger.append(pid, { source:'system', kind:'git_committed',\n ref: sha.slice(0,7), summary: spec.summary });\n sseBroker.emit(pid, 'git.committed', { sha, kind: spec.kind, summary: spec.summary });\n }\n}\n\n// Commit 时机表\nPM 完成 prd → kind:'prd', msg: 'prd: rev N · {summary}'\nArchitect generate → kind:'sdd', msg: 'sdd: rev N · X units · ...'\nArchitect patch → kind:'sdd', msg: 'sdd: patch · {affected_units}'\nCoder + Tester 绿 → kind:'code', msg: 'code(unit-id): implement|retry · attempt N'\nCoder 单独完成 → 不 commit (等 Tester 验证)\nTester 红 → 不 commit (避免噪音)\nBugTriager → kind:'triage', msg: 'triage(run-id): {category} · confidence {n}'\nIntegrator → kind:'integ', msg: 'integ(run-id): pass|fail · X/Y'\nshipped → tag:'shipped-v{prd_rev}-{sdd_rev}'\nsign-off → tag:'final-v{prd_rev}'\nabandon → tag:'abandoned'", "contract": [ "Coder commit 推迟到 Tester 绿(避免红色代码进 main)", "所有 commit author 标 Agent 名", "nothing-to-commit 返 skipped=true,不抛错", "commit 失败 → 记 warning 但不阻塞 Agent 返回", "shipped/sign-off/abandon 用 tag 而非 commit,commit 是文件变化,tag 是项目里程碑" ], "tests": [ "项目创建后 git log 含 init commit + .gitignore", "PM 完成后多 1 个 prd: rev 1 commit", "Coder retry 3 次但只 Tester 绿一次 → 仅 1 个 code commit", "shipped → tag shipped-v1-1 存在" ] } ] }