sdd-process.html
43 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
613
614
615
616
617
618
619
620
621
622
623
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SDD · Process · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="sdd-process.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">03 · SDD</span><span class="sep">/</span><span>P · Process</span></div>
<h1>Process · 长进程与状态机</h1>
<p class="deck">所有「不是即时响应」的代码:三个 Loop 的状态机详尽转换表、Agent 生命周期、SSE 心跳与 replay、retryWithReflection、清理与归档。</p>
</header>
<article class="body">
<section data-def-name="Process.L1" data-mvcp="process">
<h2>P-1 · L1 Unit Loop 状态机</h2>
<code class="path mono" style="color:var(--ink-soft)">loop-controller.js → runUnit()</code>
<h3>转换表(完整)</h3>
<table class="dense">
<thead><tr><th>from</th><th>to</th><th>触发</th><th>副作用</th></tr></thead>
<tbody>
<tr><td>pending</td><td>coding</td><td>LoopController 调度到 wave</td><td>markCoding + SSE unit.state</td></tr>
<tr><td>coding</td><td>testing</td><td>Coder 返回 success</td><td>markTesting + SSE</td></tr>
<tr><td>coding</td><td>failed</td><td>Coder 抛错(沙箱越权 / 超时)</td><td>fail · reason=coder_error</td></tr>
<tr><td>testing</td><td>passed</td><td>Tester result.ok = true</td><td>pass + 写 contract_hash + SSE</td></tr>
<tr><td>testing</td><td>failed</td><td>Tester result.ok = false</td><td>fail · 写 result.json</td></tr>
<tr><td>failed</td><td>coding</td><td>retries < retry_budget</td><td>retries++ · 调 Coder 带 history</td></tr>
<tr><td>failed</td><td>blocked</td><td>retries ≥ retry_budget</td><td>block + SSE human_intervention_needed</td></tr>
<tr><td>passed</td><td>pending</td><td>L2 触发 reset</td><td>retries 重置 · 旧代码归档</td></tr>
<tr><td>blocked</td><td>pending</td><td>人类显式 resume</td><td>retries 重置</td></tr>
</tbody>
</table>
<h3>伪代码</h3>
<pre class="code shape"><span class="k">async function</span> <span class="n">runUnit</span>(pid, unitId) <span class="p">{</span>
<span class="k">const</span> unit = unitRegistry.<span class="n">list</span>(pid).<span class="n">find</span>(u => u.<span class="n">id</span> === unitId);
<span class="k">let</span> failures = <span class="p">[]</span>;
<span class="k">for</span> (<span class="k">let</span> i = unit.<span class="n">retries</span>; i < unit.<span class="n">retry_budget</span>; i++) <span class="p">{</span>
unitRegistry.<span class="n">markCoding</span>(pid, unitId);
<span class="k">const</span> coderRes = <span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'coder'</span>, <span class="p">{</span>
pid, unit_id: unitId, history: failures
<span class="p">}</span>);
<span class="k">if</span> (!coderRes.<span class="n">ok</span>) <span class="p">{</span> unitRegistry.<span class="n">fail</span>(pid, unitId, <span class="s">'coder_error'</span>); <span class="k">continue</span>; <span class="p">}</span>
unitRegistry.<span class="n">markTesting</span>(pid, unitId);
<span class="k">const</span> testerRes = <span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'tester'</span>, <span class="p">{</span> pid, unit_id: unitId <span class="p">}</span>);
<span class="k">if</span> (testerRes.<span class="n">ok</span>) <span class="p">{</span>
<span class="k">const</span> hash = docStore.<span class="n">hashContract</span>(<span class="k">await</span> readContract(pid, unitId));
unitRegistry.<span class="n">pass</span>(pid, unitId, hash);
<span class="k">return</span> <span class="p">{</span> ok: <span class="v">true</span>, retries: i <span class="p">}</span>;
<span class="p">}</span>
failures.<span class="n">push</span>(testerRes.<span class="n">failures</span>);
unitRegistry.<span class="n">fail</span>(pid, unitId, <span class="s">'test_red'</span>);
<span class="p">}</span>
unitRegistry.<span class="n">block</span>(pid, unitId, <span class="s">'budget_exceeded'</span>);
<span class="k">return</span> <span class="p">{</span> ok: <span class="v">false</span> <span class="p">}</span>;
<span class="p">}</span></pre>
<ul class="contract">
<li>同一 Unit 任何时刻只允许一个 runUnit 实例(用 Map<unitId, Promise> 去重)</li>
<li>retry 之间不复用 Agent session(每次新 ACP session,保证干净)</li>
</ul>
</section>
<section data-def-name="Process.L2" data-mvcp="process">
<h2>P-2 · L2 Integration Loop 状态机</h2>
<code class="path mono" style="color:var(--ink-soft)">loop-controller.js → onL2Failure() · runProject() L2 段</code>
<h3>L2 闸门判定</h3>
<pre class="code shape"><span class="k">function</span> <span class="n">l2GateOpen</span>(pid) <span class="p">{</span>
<span class="k">const</span> units = unitRegistry.<span class="n">list</span>(pid);
<span class="k">const</span> sdd = docStore.<span class="n">readSDD</span>(pid);
<span class="k">return</span> units.<span class="n">every</span>(u => <span class="p">{</span>
<span class="k">if</span> (u.<span class="n">status</span> !== <span class="s">'passed'</span>) <span class="k">return</span> <span class="v">false</span>;
<span class="k">const</span> expectedHash = contractHashFromSDD(sdd, u.<span class="n">id</span>);
<span class="k">return</span> u.<span class="n">contract_hash</span> === expectedHash;
<span class="p">}</span>);
<span class="p">}</span></pre>
<h3>L2 主循环</h3>
<pre class="code shape"><span class="k">async function</span> <span class="n">runL2</span>(pid) <span class="p">{</span>
<span class="k">const</span> intRes = <span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'integrator'</span>, <span class="p">{</span> pid <span class="p">}</span>);
<span class="k">if</span> (intRes.<span class="n">ok</span>) <span class="p">{</span>
sseBroker.<span class="n">emit</span>(pid, <span class="s">'project.shipped'</span>, <span class="p">{</span>}); <span class="k">return</span> <span class="s">'shipped'</span>;
<span class="p">}</span>
<span class="k">const</span> tri = <span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'bug-triager'</span>, <span class="p">{</span>
pid, run_id: intRes.<span class="n">run_id</span>
<span class="p">}</span>);
<span class="k">if</span> (tri.<span class="n">confidence</span> < <span class="v">0.5</span> || tri.<span class="n">category</span> === <span class="s">'spec_ambiguous'</span>) <span class="p">{</span>
sseBroker.<span class="n">emit</span>(pid, <span class="s">'human_intervention_needed'</span>, tri);
<span class="k">return</span> <span class="s">'l2_blocked'</span>;
<span class="p">}</span>
<span class="k">if</span> (tri.<span class="n">category</span> === <span class="s">'infra_flake'</span>) <span class="p">{</span>
<span class="k">return</span> <span class="n">runL2</span>(pid); <span class="c">// 直接重跑,不动 Unit</span>
<span class="p">}</span>
<span class="k">if</span> (tri.<span class="n">category</span> === <span class="s">'impl_bug'</span>) <span class="p">{</span>
<span class="c">// 不改 SDD,只 reset 受影响 Unit</span>
unitRegistry.<span class="n">reset</span>(pid, tri.<span class="n">affected_units</span>);
<span class="k">return</span> <span class="s">'retry_l1'</span>;
<span class="p">}</span>
<span class="c">// contract_mismatch → patch SDD + reset</span>
<span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'architect'</span>, <span class="p">{</span>
pid, mode: <span class="s">'patch'</span>, patch: tri.<span class="n">proposed_sdd_patch</span>
<span class="p">}</span>);
unitRegistry.<span class="n">reset</span>(pid, tri.<span class="n">affected_units</span>);
<span class="k">const</span> project = <span class="n">getProjectMeta</span>(pid);
<span class="k">if</span> (++project.<span class="n">l2_rounds</span> >= project.<span class="n">l2_budget</span>) <span class="p">{</span>
sseBroker.<span class="n">emit</span>(pid, <span class="s">'project.exhausted'</span>, <span class="p">{</span> last_triage: tri <span class="p">}</span>);
<span class="k">return</span> <span class="s">'l2_exhausted'</span>;
<span class="p">}</span>
<span class="k">return</span> <span class="s">'retry_l1'</span>;
<span class="p">}</span></pre>
<h3>四种 triage 类别的分支</h3>
<table class="dense">
<thead><tr><th>category</th><th>是否改 SDD</th><th>是否 reset Unit</th><th>是否计入 l2_rounds</th></tr></thead>
<tbody>
<tr><td>contract_mismatch</td><td>是</td><td>是</td><td>是</td></tr>
<tr><td>impl_bug</td><td>否</td><td>是</td><td>否(不烧预算)</td></tr>
<tr><td>spec_ambiguous</td><td>否(升 L3)</td><td>否</td><td>否</td></tr>
<tr><td>infra_flake</td><td>否</td><td>否</td><td>否</td></tr>
</tbody>
</table>
</section>
<section data-def-name="Process.L3" data-mvcp="process">
<h2>P-3 · L3 Human Feedback 队列</h2>
<code class="path mono" style="color:var(--ink-soft)">loop-controller.js → onHumanFeedback()</code>
<pre class="code shape"><span class="c">// 每个项目维护一个反馈队列,串行消费</span>
<span class="k">const</span> feedbackQueues = <span class="k">new</span> <span class="n">Map</span><<span class="n">string</span>, Feedback[]>();
<span class="k">function</span> <span class="n">onHumanFeedback</span>(pid, feedback) <span class="p">{</span>
<span class="k">if</span> (!feedbackQueues.<span class="n">has</span>(pid)) feedbackQueues.<span class="n">set</span>(pid, <span class="p">[]</span>);
feedbackQueues.<span class="n">get</span>(pid).<span class="n">push</span>(feedback);
<span class="c">// 单 worker per pid</span>
<span class="k">if</span> (!isWorking(pid)) drainFeedback(pid);
<span class="p">}</span>
<span class="k">async function</span> <span class="n">drainFeedback</span>(pid) <span class="p">{</span>
<span class="k">while</span> (feedbackQueues.<span class="n">get</span>(pid).<span class="n">length</span>) <span class="p">{</span>
<span class="k">const</span> fb = feedbackQueues.<span class="n">get</span>(pid).<span class="n">shift</span>();
<span class="c">// 1. 等当前 L1 / L2 跑完(不打断)</span>
<span class="k">await</span> waitForCurrentLoop(pid);
<span class="c">// 2. PM 增量补 PRD</span>
<span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'pm'</span>, <span class="p">{</span> pid, feedback: fb <span class="p">}</span>);
<span class="c">// 3. Architect 重算 SDD + units.json (mode=generate)</span>
<span class="k">await</span> agentManager.<span class="n">callAgent</span>(<span class="s">'architect'</span>, <span class="p">{</span> pid, mode: <span class="s">'generate'</span> <span class="p">}</span>);
<span class="c">// 4. UnitRegistry.diffWithSDD → reset changed/removed</span>
<span class="k">const</span> diff = unitRegistry.<span class="n">diffWithSDD</span>(pid);
unitRegistry.<span class="n">reset</span>(pid, [...diff.<span class="n">changed</span>, ...diff.<span class="n">removed</span>]);
<span class="c">// 5. 触发 runProject(增量 L1 + L2)</span>
runProject(pid);
<span class="p">}</span>
<span class="p">}</span></pre>
<ul class="contract">
<li>同一 pid 永远只有一个 worker 在 drain(保证文档一致性)</li>
<li>多 pid 之间互不阻塞</li>
<li>worker 崩了,重启 server 后会自动 resume(从 stream.log 反推队列状态)</li>
</ul>
</section>
<section data-def-name="Process.AgentLifecycle" data-mvcp="process">
<h2>P-4 · Agent 生命周期</h2>
<code class="path mono" style="color:var(--ink-soft)">server/src/core/session-pool.js · packages/acp-core/protocol/ClaudeCliProtocol.js · 复用既有</code>
<pre class="code shape"><span class="c">// 复用 LemonBoss SessionPool</span>
<span class="c">// key = (agentName, cid, pid)</span>
<span class="c">// 1. 首次 callAgent</span>
<span class="c">// ↓ SessionPool 检查无该 key</span>
<span class="c">// ↓ 通过 ProcessLauncher 起一个 claude CLI 子进程</span>
<span class="c">// ↓ 走 ClaudeCliProtocol.initialize() + newSession()</span>
<span class="c">// ↓ 注入 system prompt (system/agents/devloop-*/prompt.md)</span>
<span class="c">// ↓ sendPrompt(input)</span>
<span class="c">// ↓ 流式累积 → AgentResult</span>
<span class="c">// 2. 第二次同 key 的 callAgent</span>
<span class="c">// ↓ SessionPool 命中,复用 session</span>
<span class="c">// ↓ sendPrompt(input) ← 上下文延续</span>
<span class="c">// 3. 项目结束(shipped/abandoned)</span>
<span class="c">// ↓ SessionPool.closeProject(pid)</span>
<span class="c">// ↓ 杀掉所有该 pid 的 claude 进程</span>
<span class="c">// 4. 项目空闲 30 分钟</span>
<span class="c">// ↓ 自动 sweep 该 pid 的 session(省内存)</span>
<span class="c">// ↓ 下次 callAgent 重启冷启 OK</span></pre>
<ul class="contract">
<li>每个 Agent 进程绑死自己的 cwd(沙箱)</li>
<li>Bash 工具受 jsonGuard 中间件保护(防 inline JSON)</li>
<li>process-guard 看护:5s SIGTERM → 10s SIGKILL</li>
</ul>
</section>
<section data-def-name="Process.SSE" data-mvcp="process">
<h2>P-5 · SSE Broker 心跳与 Replay</h2>
<code class="path mono" style="color:var(--ink-soft)">server/services/sse-broker.js</code>
<pre class="code shape"><span class="c">// 每订阅一个 pid,open 一份连接列表</span>
<span class="c">// subscriptions: Map<pid, Set<ServerResponse>></span>
<span class="k">function</span> <span class="n">subscribe</span>(pid, res, opts) <span class="p">{</span>
subscriptions.<span class="n">get</span>(pid)?.<span class="n">add</span>(res) || subscriptions.<span class="n">set</span>(pid, <span class="k">new</span> <span class="n">Set</span>([res]));
<span class="c">// Replay:从 stream.log 重放给当前订阅者</span>
<span class="k">if</span> (opts?.<span class="n">replayFromOffset</span>) replayTo(res, pid, opts.<span class="n">replayFromOffset</span>);
<span class="c">// 心跳</span>
<span class="k">const</span> heartbeat = <span class="n">setInterval</span>(() => res.<span class="n">write</span>(<span class="s">':keepalive\n\n'</span>), <span class="v">25_000</span>);
res.<span class="n">on</span>(<span class="s">'close'</span>, () => <span class="p">{</span> <span class="n">clearInterval</span>(heartbeat); subscriptions.<span class="n">get</span>(pid).<span class="n">delete</span>(res); <span class="p">}</span>);
<span class="p">}</span>
<span class="k">function</span> <span class="n">emit</span>(pid, event, data) <span class="p">{</span>
<span class="k">const</span> id = ++eventCounter;
<span class="k">const</span> line = <span class="s">`id: </span><span class="v">${id}</span><span class="s">\nevent: </span><span class="v">${event}</span><span class="s">\ndata: </span><span class="v">${JSON.stringify(data)}</span><span class="s">\n\n`</span>;
<span class="c">// 1. 落盘 stream.log</span>
fs.<span class="n">appendFileSync</span>(streamLogPath(pid), line);
<span class="c">// 2. 广播给所有订阅者</span>
<span class="k">for</span> (<span class="k">const</span> res <span class="k">of</span> subscriptions.<span class="n">get</span>(pid) || <span class="p">[]</span>) res.<span class="n">write</span>(line);
<span class="p">}</span></pre>
<ul class="contract">
<li>stream.log 保留 7 天,期间任何时刻 replay 都能精确</li>
<li>同事件不重复广播(去重以 event id 为准)</li>
</ul>
</section>
<section data-def-name="Process.Cleanup" data-mvcp="process">
<h2>P-6 · 清理与归档</h2>
<code class="path mono" style="color:var(--ink-soft)">分布在 doc-store.js · unit-registry.js · sse-broker.js · scripts/devloop-cleanup-cron.js (NEW)</code>
<pre class="code shape"><span class="c">// 清理钩子标准签名(被各服务调用 + 一个日 cron)</span>
<span class="k">type</span> CleanupHook = (pid: <span class="n">string</span>) => <span class="n">Promise</span><<span class="n">void</span>>;
<span class="c">// cron: scripts/devloop-cleanup-cron.js · 每日 04:00 跑</span></pre>
<table class="dense">
<thead><tr><th>触发</th><th>动作</th><th>实现位置</th></tr></thead>
<tbody>
<tr><td>DocStore.write*</td><td>旧版本 → history/</td><td>doc-store.js</td></tr>
<tr><td>UnitRegistry.reset</td><td>units/{id}/code 旧版 → history/removed/{id}-{ts}/</td><td>unit-registry.js</td></tr>
<tr><td>project.shipped 30 天后</td><td>项目目录置 .archived 标记</td><td>日 cron 脚本</td></tr>
<tr><td>project.json.compressOldRuns=true</td><td>runs/ 14 天前的 stdout.txt → .gz</td><td>同上</td></tr>
<tr><td>SSE close</td><td>清 subscriptions 项</td><td>sse-broker.js</td></tr>
<tr><td>Server SIGTERM</td><td>kill 所有 ACP 子进程</td><td>process-guard 既有</td></tr>
</tbody>
</table>
</section>
<section data-def-name="Process.Budget" data-mvcp="process">
<h2>P-7 · 预算守护</h2>
<code class="path mono" style="color:var(--ink-soft)">server/middleware/token-accountant.js (NEW) · 阈值嵌入 loop-controller.js</code>
<pre class="code shape"><span class="c">// 预算配置 schema(落在 project.json)</span>
<span class="k">type</span> ProjectBudgets = <span class="p">{</span>
retry_budget: <span class="n">number</span>; <span class="c">// 默认 5 · 每 Unit</span>
l2_budget: <span class="n">number</span>; <span class="c">// 默认 3 · 每 PRD rev</span>
token_budget: <span class="n">number</span>; <span class="c">// 默认 5_000_000 · 项目</span>
agent_wallclock_ms:<span class="n">number</span>; <span class="c">// 默认 600_000 · 单 Agent 调用</span>
integrator_wallclock_ms: <span class="n">number</span>; <span class="c">// 默认 300_000 · 单 L2 run</span>
<span class="p">}</span>;</pre>
<table class="dense">
<thead><tr><th>预算</th><th>阈值</th><th>超出后</th></tr></thead>
<tbody>
<tr><td>retry_budget(每 Unit)</td><td>默认 5</td><td>该 Unit → blocked</td></tr>
<tr><td>l2_budget(每 PRD rev)</td><td>默认 3</td><td>项目 → l2_exhausted</td></tr>
<tr><td>token_budget(每项目)</td><td>默认 5_000_000</td><td>项目 → blocked + SSE 告警</td></tr>
<tr><td>Agent 单次墙钟</td><td>默认 10 min</td><td>kill + 计 fail 一次</td></tr>
<tr><td>Integrator 总墙钟</td><td>默认 5 min</td><td>kill + 算 infra_flake</td></tr>
</tbody>
</table>
<p class="sig">所有阈值在 <code>project.json</code> 可覆盖。生产建议保持默认。</p>
</section>
<section data-def-name="Process.StatePersistence" data-mvcp="process">
<h2>P-8 · Agent State 持久化 + 崩溃恢复</h2>
<code class="path mono" style="color:var(--ink-soft)">loop-controller.js · agent-state/*.json · timeline.ndjson</code>
<h3>调用前 / 中 / 后三阶段</h3>
<pre class="code shape"><span class="c">// LoopController.callAgent 的完整包裹流程</span>
<span class="k">async function</span> <span class="n">callAgentWithPersistence</span>(name, input) <span class="p">{</span>
<span class="k">const</span> call_id = <span class="n">genCallId</span>();
<span class="k">const</span> statePath = <span class="s">`</span><span class="v">${pidDir}</span><span class="s">/agent-state/</span><span class="v">${name}</span><span class="s">.json`</span>;
<span class="c">// ── 调用前 ──</span>
<span class="k">const</span> inputSnap = <span class="k">await</span> <span class="n">snapshot</span>(input); <span class="c">// → agent-state/snapshots/{call_id}-in.json</span>
fs.<span class="n">writeFileSync</span>(statePath, <span class="n">JSON.stringify</span>(<span class="p">{</span>
agent: name, call_id, status: <span class="s">'running'</span>,
current_step: <span class="v">0</span>, total_steps: <span class="v">0</span>, <span class="c">// Agent 自己稍后会写</span>
last_input_ref: inputSnap,
started_at: now(), updated_at: now(),
<span class="p">}</span>));
timelineLogger.<span class="n">append</span>(pid, <span class="p">{</span>
source: <span class="s">'system'</span>, kind: <span class="s">'agent_started'</span>,
ref: <span class="s">`agent/</span><span class="v">${name}</span><span class="s">/</span><span class="v">${call_id}</span><span class="s">`</span>,
summary: <span class="s">`</span><span class="v">${name}</span><span class="s"> started (unit=</span><span class="v">${input.unit_id || '-'}</span><span class="s">)`</span>
<span class="p">}</span>);
<span class="k">try</span> <span class="p">{</span>
<span class="c">// ── 调用中 ── (Agent 自己通过工具更新)</span>
<span class="c">// - record_memory(topic, content, relevance)</span>
<span class="c">// - write_todolist(items) → agent-state/{agent}-todo.json</span>
<span class="c">// - update_todo_item(id, status, ref) → 更新单条 item + 同步 progress_pct</span>
<span class="c">// - update_runtime_state(current_step) → 修 agent-state/{agent}.json</span>
<span class="c">// - timeline_append(kind, ref, summary)→ 写 timeline.ndjson</span>
<span class="k">const</span> result = <span class="k">await</span> agentManager.<span class="n">callAgent</span>(name, input);
<span class="c">// ── 调用后 (成功) ──</span>
<span class="k">const</span> outSnap = <span class="k">await</span> <span class="n">snapshot</span>(result);
fs.<span class="n">writeFileSync</span>(statePath, <span class="n">JSON.stringify</span>(<span class="p">{</span>
...readState(statePath),
status: <span class="s">'done'</span>,
last_output_ref: outSnap,
updated_at: now(),
<span class="p">}</span>));
timelineLogger.<span class="n">append</span>(pid, <span class="p">{</span>
source: name, kind: <span class="s">'agent_done'</span>,
ref: <span class="s">`agent/</span><span class="v">${name}</span><span class="s">/</span><span class="v">${call_id}</span><span class="s">`</span>,
summary: <span class="s">`</span><span class="v">${name}</span><span class="s"> done in </span><span class="v">${result.duration_ms}</span><span class="s">ms`</span>,
payload: <span class="p">{</span> tokens: result.tokens_used <span class="p">}</span>
<span class="p">}</span>);
<span class="k">return</span> result;
<span class="p">}</span> <span class="k">catch</span> (e) <span class="p">{</span>
<span class="c">// ── 调用后 (失败) ──</span>
fs.<span class="n">writeFileSync</span>(statePath, <span class="n">JSON.stringify</span>(<span class="p">{</span>
...readState(statePath),
status: <span class="s">'error'</span>,
error: <span class="p">{</span> code: e.code, message: e.message, stack: e.stack <span class="p">}</span>,
updated_at: now(),
<span class="p">}</span>));
timelineLogger.<span class="n">append</span>(pid, <span class="p">{</span>
source: <span class="s">'system'</span>, kind: <span class="s">'agent_error'</span>,
ref: <span class="s">`agent/</span><span class="v">${name}</span><span class="s">/</span><span class="v">${call_id}</span><span class="s">`</span>,
summary: e.message
<span class="p">}</span>);
<span class="k">throw</span> e;
<span class="p">}</span>
<span class="p">}</span></pre>
<h3>崩溃恢复流程</h3>
<pre class="code shape"><span class="c">// server 启动时调用一次</span>
<span class="k">async function</span> <span class="n">recoverOnBoot</span>() <span class="p">{</span>
<span class="k">const</span> projects = <span class="n">listAllProjects</span>(); <span class="c">// 扫 data/companies/*/projects/*/</span>
<span class="k">for</span> (<span class="k">const</span> pid <span class="k">of</span> projects) <span class="p">{</span>
<span class="k">const</span> states = <span class="n">readAgentStates</span>(pid); <span class="c">// 读全部 agent-state/*.json</span>
<span class="k">for</span> (<span class="k">const</span> s <span class="k">of</span> states) <span class="p">{</span>
<span class="k">if</span> (s.status === <span class="s">'running'</span>) <span class="p">{</span>
<span class="c">// 之前的进程被杀,改 paused 等待人决策</span>
<span class="n">writeState</span>(s.agent, <span class="p">{</span>
...s,
status: <span class="s">'paused'</span>,
resume_token: <span class="n">genResumeToken</span>(),
updated_at: now()
<span class="p">}</span>);
timelineLogger.<span class="n">append</span>(pid, <span class="p">{</span>
source: <span class="s">'system'</span>, kind: <span class="s">'agent_paused_by_recovery'</span>,
ref: <span class="s">`agent/</span><span class="v">${s.agent}</span><span class="s">/</span><span class="v">${s.call_id}</span><span class="s">`</span>,
summary: <span class="s">'Server 重启,Agent 已挂起等待决策'</span>
<span class="p">}</span>);
sseBroker.<span class="n">emit</span>(pid, <span class="s">'resume_decision_needed'</span>, <span class="p">{</span>
agent: s.agent, call_id: s.call_id,
last_step: s.current_step, total: s.total_steps,
options: <span class="p">[</span><span class="s">'resume_from_step'</span>, <span class="s">'restart_fresh'</span>, <span class="s">'abort'</span><span class="p">]</span>
<span class="p">}</span>);
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></pre>
<h3>恢复决策三态</h3>
<table class="dense">
<thead><tr><th>option</th><th>含义</th><th>动作</th></tr></thead>
<tbody>
<tr><td><code>resume_from_step</code></td><td>从 current_step 继续</td><td>读 last_input_ref + todolist,重新 callAgent,input 携带 history</td></tr>
<tr><td><code>restart_fresh</code></td><td>整个 Unit 重写</td><td>UnitRegistry.reset 该 unit,清 todolist,回 L1 队列</td></tr>
<tr><td><code>abort</code></td><td>放弃</td><td>UnitRegistry.block 该 unit,等下一轮人类反馈</td></tr>
</tbody>
</table>
<h3>Memory 注入流程</h3>
<pre class="code shape"><span class="c">// 每次 Agent 启动前,把相关 memory 注入 system prompt</span>
<span class="k">function</span> <span class="n">buildPromptWithMemory</span>(agent, basePrompt) <span class="p">{</span>
<span class="k">const</span> memDir = <span class="s">`</span><span class="v">${companyDir}</span><span class="s">/memory/devloop/</span><span class="v">${agent}</span><span class="s">/`</span>;
<span class="k">if</span> (!fs.<span class="n">existsSync</span>(memDir)) <span class="k">return</span> basePrompt;
<span class="k">const</span> memories = fs.<span class="n">readdirSync</span>(memDir)
.<span class="n">map</span>(f => <span class="n">parseMemory</span>(path.<span class="n">join</span>(memDir, f)))
.<span class="n">filter</span>(m => m.relevance_score >= <span class="v">0.5</span>)
.<span class="n">sort</span>((a, b) => b.relevance_score - a.relevance_score)
.<span class="n">slice</span>(<span class="v">0</span>, <span class="v">5</span>);
<span class="k">if</span> (memories.length === <span class="v">0</span>) <span class="k">return</span> basePrompt;
<span class="k">const</span> memSection = <span class="s">`## Memory (from prior projects)\n`</span> +
memories.<span class="n">map</span>(m => <span class="s">`### </span><span class="v">${m.topic}</span><span class="s">\n</span><span class="v">${m.content}</span><span class="s">`</span>).<span class="n">join</span>(<span class="s">'\n\n'</span>);
<span class="k">return</span> basePrompt + <span class="s">'\n\n'</span> + memSection;
<span class="p">}</span></pre>
<ul class="contract">
<li>statePath 写入是<b>原子的</b>(fs.writeFileSync + sync flag)</li>
<li>resume_token 有效期 24 小时,过期视同 restart_fresh</li>
<li>memory 注入不超过 5 条,避免 prompt 膨胀</li>
<li>relevance < 0.5 的 memory 不注入但保留文件</li>
<li>同一 (pid, agent) 任意时刻只允许一个 status=running</li>
</ul>
<h3>触发 SSE 事件</h3>
<table class="dense">
<thead><tr><th>事件</th><th>何时</th></tr></thead>
<tbody>
<tr><td><code>agent.started</code></td><td>callAgentWithPersistence 进入</td></tr>
<tr><td><code>agent.todolist</code></td><td>Agent write_todolist 后</td></tr>
<tr><td><code>agent.step</code></td><td>Agent update_todo_item 后(前端用于 「3/5」)</td></tr>
<tr><td><code>agent.done</code></td><td>成功结束</td></tr>
<tr><td><code>agent.error</code></td><td>抛错</td></tr>
<tr><td><code>resume_decision_needed</code></td><td>recoverOnBoot 检测到 paused</td></tr>
</tbody>
</table>
</section>
<section data-def-name="Process.BugLifecycle" data-mvcp="process">
<h2>P-9 · Bug 状态机</h2>
<code class="path mono" style="color:var(--ink-soft)">server/services/bug-tracker.js → transition()</code>
<h3>状态机</h3>
<svg viewBox="0 0 1100 240" width="100%" role="img">
<defs><marker id="arrBL" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#0E0E10"/></marker></defs>
<g font-family="JetBrains Mono" font-size="12" fill="#0E0E10">
<rect x="20" y="90" width="120" height="50" rx="4" fill="#FCE7E2" stroke="#C7341A"/><text x="80" y="120" text-anchor="middle">open</text>
<rect x="200" y="90" width="120" height="50" rx="4" fill="#FFF6CC" stroke="#8a6b00"/><text x="260" y="120" text-anchor="middle">fixing</text>
<rect x="380" y="90" width="120" height="50" rx="4" fill="#E2F2EA" stroke="#0E8C5A"/><text x="440" y="120" text-anchor="middle">fixed</text>
<rect x="560" y="90" width="120" height="50" rx="4" fill="#fff" stroke="#0E8C5A" stroke-width="2"/><text x="620" y="120" text-anchor="middle">verified</text>
<rect x="200" y="20" width="120" height="50" rx="4" fill="#EAEAEA" stroke="#666"/><text x="260" y="50" text-anchor="middle">wontfix</text>
<rect x="380" y="20" width="120" height="50" rx="4" fill="#EAEAEA" stroke="#666"/><text x="440" y="50" text-anchor="middle">duplicate</text>
<rect x="380" y="170" width="160" height="50" rx="4" fill="#FCE7E2" stroke="#C7341A"/><text x="460" y="200" text-anchor="middle">reopened → open</text>
<g stroke="#0E0E10" fill="none" marker-end="url(#arrBL)">
<line x1="140" y1="115" x2="195" y2="115"/>
<line x1="320" y1="115" x2="375" y2="115"/>
<line x1="500" y1="115" x2="555" y2="115"/>
<line x1="140" y1="100" x2="195" y2="50"/>
<line x1="140" y1="100" x2="375" y2="50"/>
<line x1="500" y1="140" x2="460" y2="165"/>
<line x1="460" y1="220" x2="80" y2="140"/>
</g>
</g>
</svg>
<h3>合法转换</h3>
<table class="dense">
<thead><tr><th>from</th><th>to</th><th>触发</th><th>副作用</th></tr></thead>
<tbody>
<tr><td>(none)</td><td>open</td><td>Tester / Integrator 失败 → bugTracker.open</td><td>+ timeline · + SSE bug.opened</td></tr>
<tr><td>open</td><td>fixing</td><td>Coder 进入 coding 态(关联同 unit)</td><td>+ status_history · + SSE</td></tr>
<tr><td>open</td><td>wontfix</td><td>人类决策</td><td>需带 note</td></tr>
<tr><td>open</td><td>duplicate</td><td>BugTracker dedupKey 命中 + 人裁决</td><td>需填 duplicate_of</td></tr>
<tr><td>fixing</td><td>fixed</td><td>同 unit Tester 绿且 fix_refs 含新 run_id</td><td>+ SSE bug.fixed</td></tr>
<tr><td>fixing</td><td>open</td><td>Coder 退出但未通过</td><td>retries++</td></tr>
<tr><td>fixed</td><td>verified</td><td>L2 Integration 全部通过</td><td>+ SSE bug.verified</td></tr>
<tr><td>fixed</td><td>open</td><td>L2 失败且 BugTriager 归因到同一 root_cause</td><td>reopened</td></tr>
<tr><td>verified</td><td>open</td><td>新 L3 反馈引入回归</td><td>reopened</td></tr>
</tbody>
</table>
<h3>自动转换 vs 人为转换</h3>
<ul>
<li><b>自动:</b>open → fixing → fixed → verified 主链由 LoopController + Tester + Integrator 触发</li>
<li><b>人为:</b>wontfix / duplicate 必须人类显式调 API + 带 note</li>
<li><b>重开:</b>fixed/verified → open 由系统自动触发但<b>必须</b>关联到具体 run_id + triage_id 证据</li>
</ul>
<h3>与 L1/L2 的耦合</h3>
<pre class="code shape"><span class="c">// L1 内:Tester 失败 → 开 bug + 设 unit failed</span>
testerResult.failures.<span class="n">forEach</span>(f => <span class="p">{</span>
bugTracker.<span class="n">open</span>(pid, <span class="p">{</span>
source: <span class="s">'unit_test'</span>, unit_id: unit.id, run_id: runId,
test_name: f.test, category: <span class="s">'unknown'</span>,
severity: <span class="s">'high'</span>, title: f.test + <span class="s">' '</span> + <span class="n">brief</span>(f),
detail: <span class="n">JSON.stringify</span>(f), affected_units: [unit.id]
<span class="p">}</span>);
<span class="p">}</span>);
<span class="c">// 下一轮 Coder 进入 → 关联 bug 状态转 fixing</span>
bugTracker.<span class="n">list</span>(pid, <span class="p">{</span> unit_id: unit.id, status: [<span class="s">'open'</span>] <span class="p">}</span>)
.<span class="n">forEach</span>(b => bugTracker.<span class="n">transition</span>(pid, b.id, <span class="p">{</span> to:<span class="s">'fixing'</span>, by:<span class="s">'coder'</span> <span class="p">}</span>));
<span class="c">// L2 通过 → 所有 fixed → verified</span>
<span class="k">if</span> (integratorResult.ok) <span class="p">{</span>
bugTracker.<span class="n">list</span>(pid, <span class="p">{</span> status: [<span class="s">'fixed'</span>] <span class="p">}</span>)
.<span class="n">forEach</span>(b => bugTracker.<span class="n">transition</span>(pid, b.id, <span class="p">{</span> to:<span class="s">'verified'</span>, by:<span class="s">'system'</span> <span class="p">}</span>));
<span class="p">}</span></pre>
<h3>新增 SSE 事件</h3>
<table class="dense">
<thead><tr><th>event</th><th>触发</th></tr></thead>
<tbody>
<tr><td><code>bug.opened</code></td><td>bugTracker.open</td></tr>
<tr><td><code>bug.transitioned</code></td><td>任何状态转换</td></tr>
<tr><td><code>bug.reopened</code></td><td>verified → open 特化事件</td></tr>
</tbody>
</table>
</section>
<section data-def-name="Process.GitCommit" data-mvcp="process">
<h2>P-10 · 自动 git commit 流程</h2>
<code class="path mono" style="color:var(--ink-soft)">server/src/core/loop-controller.js · callAgentWithPersistence 的 success 分支扩展</code>
<pre class="code shape"><span class="c">// 完整设计见 arch-git-versioning.html · 此处只放骨架</span>
<span class="k">async function</span> <span class="n">commitAfterAgent</span>(pid, name, result, input) <span class="p">{</span>
<span class="k">const</span> spec = <span class="n">buildCommitSpec</span>(name, result, input);
<span class="k">if</span> (!spec) <span class="k">return</span>; <span class="c">// 某些情况不 commit (Coder 等 Tester 绿,Tester 红不 commit)</span>
<span class="k">const</span> <span class="p">{</span> ok, sha, skipped <span class="p">}</span> = <span class="k">await</span> gitVault.<span class="n">commit</span>(pid, spec);
<span class="k">if</span> (ok && !skipped) <span class="p">{</span>
timelineLogger.<span class="n">append</span>(pid, <span class="p">{</span> source:<span class="s">'system'</span>, kind:<span class="s">'git_committed'</span>,
ref: sha.<span class="n">slice</span>(<span class="v">0</span>,<span class="v">7</span>), summary: spec.summary <span class="p">}</span>);
sseBroker.<span class="n">emit</span>(pid, <span class="s">'git.committed'</span>, <span class="p">{</span> sha, kind: spec.kind, summary: spec.summary <span class="p">}</span>);
<span class="p">}</span>
<span class="p">}</span>
<span class="c">// Commit 时机表</span>
PM 完成 prd → kind:'prd', msg: 'prd: rev N · {summary}'
Architect generate → kind:'sdd', msg: 'sdd: rev N · X units · ...'
Architect patch → kind:'sdd', msg: 'sdd: patch · {affected_units}'
Coder + Tester 绿 → kind:'code', msg: 'code(unit-id): implement|retry · attempt N'
Coder 单独完成 → 不 commit (等 Tester 验证)
Tester 红 → 不 commit (避免噪音)
BugTriager → kind:'triage', msg: 'triage(run-id): {category} · confidence {n}'
Integrator → kind:'integ', msg: 'integ(run-id): pass|fail · X/Y'
shipped → tag:'shipped-v{prd_rev}-{sdd_rev}'
sign-off → tag:'final-v{prd_rev}'
abandon → tag:'abandoned'</pre>
<ul class="contract">
<li>Coder commit 推迟到 Tester 绿(避免红色代码进 main)</li>
<li>所有 commit author 标 Agent 名</li>
<li>nothing-to-commit 返 skipped=true,不抛错</li>
<li>commit 失败 → 记 warning 但不阻塞 Agent 返回</li>
<li>shipped/sign-off/abandon 用 tag 而非 commit,commit 是文件变化,tag 是项目里程碑</li>
</ul>
<ul class="tests">
<li>项目创建后 git log 含 init commit + .gitignore</li>
<li>PM 完成后多 1 个 prd: rev 1 commit</li>
<li>Coder retry 3 次但只 Tester 绿一次 → 仅 1 个 code commit</li>
<li>shipped → tag shipped-v1-1 存在</li>
</ul>
<h3>recoverOnBoot 加 git 清理</h3>
<pre class="code shape"><span class="k">async function</span> <span class="n">recoverOnBoot</span>() <span class="p">{</span>
<span class="k">for</span> (<span class="k">const</span> pid <span class="k">of</span> <span class="n">listAllProjects</span>()) <span class="p">{</span>
<span class="c">// P-8 既有:扫 agent-state running → paused (见 P-8)</span>
<span class="n">recoverAgentStates</span>(pid);
<span class="c">// P-10 新增:扫 git 工作区脏文件</span>
<span class="k">const</span> status = <span class="k">await</span> gitVault.<span class="n">status</span>(pid);
<span class="k">if</span> (status.<span class="n">dirty</span>) <span class="p">{</span>
sseBroker.<span class="n">emit</span>(pid, <span class="s">'recovery_dirty_tree'</span>, <span class="p">{</span>
files: status.<span class="n">files</span>,
options: [<span class="s">'commit_as_wip'</span>, <span class="s">'reset_hard'</span>, <span class="s">'manual_inspect'</span>],
<span class="p">}</span>);
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></pre>
<h3>新增 SSE 事件</h3>
<table class="dense">
<thead><tr><th>event</th><th>何时</th></tr></thead>
<tbody>
<tr><td><code>git.committed</code></td><td>commitAfterAgent 成功后</td></tr>
<tr><td><code>git.tagged</code></td><td>tag 操作后(shipped/sign-off/abandon)</td></tr>
<tr><td><code>recovery_dirty_tree</code></td><td>recoverOnBoot 检测到上次 crash 留下的脏文件</td></tr>
</tbody>
</table>
</section>
<section>
<h2>三层 Loop 嵌套伪代码(最终版)</h2>
<pre class="code shape"><span class="c">// 完整调度循环(runProject 核心)</span>
<span class="k">async function</span> <span class="n">runProject</span>(pid) <span class="p">{</span>
<span class="k">while</span> (<span class="v">true</span>) <span class="p">{</span>
<span class="c">// ===== L1 =====</span>
<span class="k">const</span> waves = scheduler.<span class="n">getExecutionGroups</span>(unitRegistry.<span class="n">pending</span>(pid));
<span class="k">for</span> (<span class="k">const</span> wave <span class="k">of</span> waves) <span class="p">{</span>
<span class="k">await</span> Promise.<span class="n">all</span>(wave.<span class="n">map</span>(u => runUnit(pid, u.<span class="n">id</span>)));
<span class="p">}</span>
<span class="k">if</span> (unitRegistry.<span class="n">anyBlocked</span>(pid)) <span class="k">return</span> <span class="s">'blocked'</span>;
<span class="c">// ===== L2 =====</span>
<span class="k">if</span> (!unitRegistry.<span class="n">l2GateOpen</span>(pid)) <span class="k">continue</span>;
<span class="k">const</span> r = <span class="k">await</span> runL2(pid);
<span class="k">if</span> (r === <span class="s">'shipped'</span>) <span class="k">return</span> <span class="s">'shipped'</span>;
<span class="k">if</span> (r === <span class="s">'l2_blocked'</span>) <span class="k">return</span> <span class="s">'blocked'</span>;
<span class="k">if</span> (r === <span class="s">'l2_exhausted'</span>) <span class="k">return</span> <span class="s">'l2_exhausted'</span>;
<span class="c">// r === 'retry_l1' → 继续外 while</span>
<span class="p">}</span>
<span class="c">// L3 入口 onHumanFeedback() 异步入队,由 drainFeedback 串行调用 runProject</span>
<span class="p">}</span></pre>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>03 · SDD</div><div><b>Page</b>sdd-process.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>