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 存在"
]
}
]
}