build-manifest.json 55.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
{
  "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 <pre data-contract> 文本) 截前 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<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>",
      "contract": [
        "顶级必须是 <article data-doc=\"prd\">,每页都有",
        "每个 section 必含 <h3> + 至少一个 <p>",
        "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": "<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>",
      "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<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",
      "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 <rect> + <line>\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<string, string>\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<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};",
      "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<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};",
      "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<AgentResult>\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, '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};",
      "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<Commit[]>,\n  diff(pid, refA, refB, opts?): Promise<string>,\n  show(pid, sha): Promise<Commit + diff>,\n  listTags(pid): Promise<Tag[]>,\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 <pm@devloop.local>\"),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<unitId, Promise> 去重)",
        "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<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}",
      "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<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}",
      "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<void>;\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 存在"
      ]
    }
  ]
}