arch-git-versioning.html
33.4 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
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Git Versioning · 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="arch-git-versioning.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">05 · ARCHITECTURE</span><span class="sep">/</span><span>Git Versioning</span></div>
<h1>Git Versioning · 每次 Agent 完工 = 一个 commit</h1>
<p class="deck">把每个 DevLoop 项目变成<b>独立 git repo</b>,每个 Agent 成功完成工作 → LoopController 自动 commit。<br/>项目结束 → 打 tag。这一层补上文档之前缺的<b>真版本管理</b>——崩了能 reset,丢了能 archive,3 次 retry 能 diff。</p>
</header>
<article class="body">
<section>
<h2>① 现状 vs 提议</h2>
<div class="grid-2">
<div class="card">
<h4>之前(无 git)</h4>
<ul>
<li>文件 mtime 是唯一时间戳</li>
<li>history/ 目录是 ad-hoc 归档</li>
<li>没有 atomic snapshot</li>
<li>回滚靠 fs.rename + 手动拷贝</li>
<li>retry 之间无法 diff(旧文件被覆盖)</li>
<li>项目导出 = 拷整个目录</li>
</ul>
</div>
<div class="card l1">
<h4>提议(每项目一个 repo)</h4>
<ul>
<li>每个 Agent 完工 → <code>git commit</code></li>
<li>shipped → <code>git tag</code></li>
<li>整个 retry 链是 git log(可 diff 任意两次)</li>
<li>崩 → <code>git reset</code> 干净恢复</li>
<li>导出 → <code>git archive</code></li>
<li>未来可加 <code>git push</code> 到 GitHub(v2)</li>
</ul>
</div>
</div>
</section>
<section>
<h2>② Repo 范围决策</h2>
<table>
<thead><tr><th>方案</th><th>位置</th><th>优</th><th>劣</th></tr></thead>
<tbody>
<tr><td><b>A · 每项目一个</b> ← <b>选这个</b></td><td><code>data/companies/{cid}/projects/{pid}/.git</code></td><td>隔离强 · 删项目就删 repo · 导出干净 · 可分别 push 不同 remote</td><td>repo 数量随项目增长(每个 ~10-50MB)</td></tr>
<tr><td>B · 一个公司一个</td><td><code>data/companies/{cid}/.git</code></td><td>跨项目共享对象</td><td>跨项目混在 git log;不易导出单项目</td></tr>
<tr><td>C · 全局一个</td><td><code>data/.git</code></td><td>对象去重最大</td><td>所有 company 混;权限灾难</td></tr>
<tr><td>D · 主仓库</td><td>lemonboss/.git</td><td>不增 repo</td><td>污染 LemonBoss 主提交历史;不可接受</td></tr>
</tbody>
</table>
<p>选 A 的核心理由:DevLoop 项目本质是<b>临时工作空间</b>,shipped 后或归档或导出,独立 repo 让生命周期边界清晰。</p>
</section>
<section>
<h2>③ 落盘后的目录</h2>
<pre class="tree">
data/companies/{cid}/projects/{pid}/
├── <span class="new">.git/</span> <span class="com"># NEW · 整个项目的 git 历史</span>
├── <span class="new">.gitignore</span> <span class="com"># NEW · 项目创建时初始化</span>
├── project.json
├── prd/ <span class="com">→ tracked</span>
├── sdd/ <span class="com">→ tracked</span>
├── units.json <span class="com">→ tracked</span>
├── integration/spec.json <span class="com">→ tracked</span>
├── timeline.ndjson <span class="com">→ tracked (append-only,git OK)</span>
├── bugs.ndjson <span class="com">→ tracked</span>
├── changelog.ndjson <span class="com">→ tracked</span>
├── units/ <span class="com">→ tracked (code + tests + contract)</span>
├── integration/runs/ <span class="com">→ tracked (result.json + triage.json)</span>
├── agent-state/ <span class="com">→ <b>.gitignore</b> (高频写,运行态)</span>
├── history/ <span class="com">→ <b>.gitignore</b> (git 本身就是 history)</span>
└── stream.log <span class="com">→ <b>.gitignore</b> (SSE 流,7 天滚动)</span>
</pre>
</section>
<section>
<h2>④ .gitignore 模板</h2>
<pre class="code"><span class="c"># LemonBoss DevLoop 项目 .gitignore (项目创建时落盘)</span>
<span class="c"># 高频写的运行态,不需要版本</span>
agent-state/
<span class="c"># 真冗余 (git 本身就是 history)</span>
history/
<span class="c"># 大日志</span>
stream.log
units/*/runs/*/stdout.txt
integration/runs/*/process.log
integration/runs/*/http-trace.ndjson.gz
<span class="c"># Node / 依赖</span>
node_modules/
*.log
.DS_Store
.cache/
.tmp/
.coverage/
<span class="c"># 测试中间文件</span>
units/*/.vitest-cache/
<span class="c"># 但保留: result.json triage.json contract.json 等小 JSON</span></pre>
<p class="sig">由 GitVault.init 工具在 <code>git init</code> 后立即写入;首个 commit 就含 .gitignore。</p>
</section>
<section>
<h2>⑤ Commit 时机表</h2>
<table>
<thead><tr><th>事件</th><th>commit msg 模板</th><th>触发位置</th><th>files added</th></tr></thead>
<tbody>
<tr><td>项目创建</td><td><code>init: project {pid} · {requirement 摘要 30 字}</code></td><td>LoopController.createProject 末尾</td><td>project.json + .gitignore</td></tr>
<tr><td>PM 写完 PRD</td><td><code>prd: rev {N} · {change_summary}</code></td><td>PM Agent 成功返回后</td><td>prd/** + changelog.ndjson + timeline 增量</td></tr>
<tr><td>Architect 生成 SDD</td><td><code>sdd: rev {N} · {units_count} units · {summary}</code></td><td>Architect generate 成功后</td><td>sdd/** + units.json + integration/spec.json + changelog/timeline</td></tr>
<tr><td>Architect patch</td><td><code>sdd: patch · {affected_units 列表}</code></td><td>Architect patch 成功后</td><td>仅改的 sdd/mvcp/{x}.html + units.json (hash 更新)</td></tr>
<tr><td>Coder 实现</td><td><code>code({unit_id}): implement · attempt {N}</code></td><td>Coder 成功 + Tester 绿才 commit</td><td>units/{id}/code/** + contract.json + coder.log</td></tr>
<tr><td>Coder retry 通过</td><td><code>code({unit_id}): retry → green · attempt {N}</code></td><td>Tester 绿(attempt > 1)</td><td>同上</td></tr>
<tr><td>Tester 写测试</td><td><b>合并到上一条 commit</b></td><td>同 Coder commit 一起</td><td>tests/ + runs/{}/result.json 一起</td></tr>
<tr><td>BugTriager 归因</td><td><code>triage({run_id}): {category} · confidence {n}</code></td><td>Triager 写完 triage.json 后</td><td>integration/runs/{}/triage.json + bugs.ndjson 增量</td></tr>
<tr><td>Integrator 跑</td><td><code>integ({run_id}): {ok|fail} · {cases_passed}/{cases_total}</code></td><td>Integrator 完工</td><td>integration/runs/{}/result.json + process.log(如不在 ignore)</td></tr>
<tr><td>Unit reset (L2 修复后)</td><td><b>不 commit</b>,下一次 Coder commit 时一起</td><td>—</td><td>—</td></tr>
<tr><td>项目 shipped</td><td><code>ship: prd@{prd_rev} sdd@{sdd_rev}</code><br/>+ <code>git tag shipped-{rev}</code></td><td>LoopController.shipProject</td><td>—(tag only)</td></tr>
<tr><td>项目 sign-off</td><td><code>chore: sign-off by {user_id}</code><br/>+ <code>git tag final-{rev}</code></td><td>POST /sign-off</td><td>project.json (status=shipped-final)</td></tr>
<tr><td>项目 abandon</td><td><code>chore: abandon · {reason}</code><br/>+ <code>git tag abandoned</code></td><td>POST /abandon</td><td>project.json (status=abandoned)</td></tr>
</tbody>
</table>
<p class="sig">关键决策:<b>失败不 commit</b>(避免噪音 + 失败的代码不该留在 git 主线)。Tester 红 → Coder retry 时旧代码被工作区覆盖,反正没人需要回到失败态。</p>
</section>
<section>
<h2>⑥ 谁调 git · 集中控制</h2>
<p>关键原则:<b>Agent 不动 git</b>。LoopController 在 callAgentWithPersistence 的 success 分支统一调 GitVault.commit。</p>
<svg viewBox="0 0 1100 280" width="100%" role="img">
<defs><marker id="arrGV" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#0E0E10"/></marker></defs>
<g font-family="JetBrains Mono" font-size="11" fill="#0E0E10">
<rect x="40" y="40" width="200" height="60" rx="6" fill="#fff" stroke="#0E0E10"/>
<text x="140" y="65" text-anchor="middle" font-weight="700">Agent (PM/Coder/...)</text>
<text x="140" y="83" text-anchor="middle" font-size="10">写 prd/ · code/ · 等</text>
<rect x="280" y="40" width="240" height="60" rx="6" fill="#FCE7E2" stroke="#C7341A" stroke-width="2"/>
<text x="400" y="65" text-anchor="middle" font-weight="700">LoopController</text>
<text x="400" y="83" text-anchor="middle" font-size="10">callAgentWithPersistence</text>
<rect x="560" y="40" width="200" height="60" rx="6" fill="#E2F2EA" stroke="#0E8C5A"/>
<text x="660" y="65" text-anchor="middle" font-weight="700">GitVault · NEW</text>
<text x="660" y="83" text-anchor="middle" font-size="10">commit · tag · log</text>
<rect x="800" y="40" width="240" height="60" rx="6" fill="#fff" stroke="#0E0E10"/>
<text x="920" y="65" text-anchor="middle" font-weight="700">.git/</text>
<text x="920" y="83" text-anchor="middle" font-size="10">objects + refs</text>
<rect x="280" y="160" width="240" height="40" rx="6" fill="#EAEAEA" stroke="#666"/>
<text x="400" y="185" text-anchor="middle">timeline_append('git_committed')</text>
<rect x="560" y="160" width="200" height="40" rx="6" fill="#F7E8D2" stroke="#C77C2C"/>
<text x="660" y="185" text-anchor="middle">SSE: git.committed</text>
<g stroke="#0E0E10" fill="none" marker-end="url(#arrGV)">
<line x1="240" y1="70" x2="275" y2="70"/>
<line x1="520" y1="70" x2="555" y2="70"/>
<line x1="760" y1="70" x2="795" y2="70"/>
<line x1="660" y1="100" x2="400" y2="155"/>
<line x1="660" y1="100" x2="660" y2="155"/>
</g>
<text x="180" y="60" font-size="9" fill="#3B3B40">files only</text>
<text x="450" y="60" font-size="9" fill="#3B3B40">commit({pid,kind,...})</text>
<text x="780" y="60" font-size="9" fill="#3B3B40">git ops</text>
</g>
</svg>
<p>这样 Agent 沙箱继续严格(不允许 Bash 调 git),但项目状态<b>真实有版本</b>。</p>
</section>
<section data-def-name="GitVault" data-mvcp="control">
<h2>⑦ GitVault · C-18 服务方法</h2>
<code class="path mono" style="color:var(--ink-soft)">server/services/git-vault.js</code>
<pre class="code shape"><span class="k">module.exports</span>.gitVault = <span class="p">{</span>
<span class="c">// 初始化(项目创建时)</span>
<span class="n">init</span>(pid: <span class="n">string</span>, opts?: <span class="p">{</span>
name?: <span class="n">string</span>, email?: <span class="n">string</span>, <span class="c">// 默认 "DevLoop <devloop@lemonboss.local>"</span>
initialBranch?: <span class="n">string</span>, <span class="c">// 默认 "main"</span>
<span class="p">}</span>): <span class="n">Promise</span><<span class="p">{</span> ok: <span class="n">boolean</span>, head: <span class="n">string</span> <span class="p">}</span>>,
<span class="c">// 主提交入口 (LoopController 调)</span>
<span class="n">commit</span>(pid: <span class="n">string</span>, <span class="p">{</span>
kind: <span class="s">'init'</span> | <span class="s">'prd'</span> | <span class="s">'sdd'</span> | <span class="s">'code'</span> | <span class="s">'triage'</span> | <span class="s">'integ'</span> | <span class="s">'ship'</span> | <span class="s">'chore'</span>,
unit_id?: <span class="n">string</span>, <span class="c">// code 时填</span>
run_id?: <span class="n">string</span>, <span class="c">// triage / integ 时填</span>
summary: <span class="n">string</span>, <span class="c">// 简短描述</span>
files?: <span class="n">string</span>[], <span class="c">// 限定 add 的路径 (默认 git add -A 受 .gitignore)</span>
author?: <span class="p">{</span> name, email <span class="p">}</span>, <span class="c">// 哪个 Agent 干的,记到 commit author</span>
skipEmpty?: <span class="n">boolean</span>, <span class="c">// nothing-to-commit 时是否抛错,默认 false</span>
<span class="p">}</span>): <span class="n">Promise</span><<span class="p">{</span> ok, sha?: <span class="n">string</span>, skipped?: <span class="n">boolean</span> <span class="p">}</span>>,
<span class="c">// 打 tag</span>
<span class="n">tag</span>(pid, tagName: <span class="n">string</span>, opts?: <span class="p">{</span> message?, sha? <span class="p">}</span>):
<span class="n">Promise</span><<span class="p">{</span> ok, tag <span class="p">}</span>>,
<span class="c">// 查询</span>
<span class="n">log</span>(pid, opts?: <span class="p">{</span> limit?, since?, grep? <span class="p">}</span>): <span class="n">Promise</span><Commit[]>,
<span class="n">diff</span>(pid, refA: <span class="n">string</span>, refB: <span class="n">string</span>, opts?: <span class="p">{</span> path? <span class="p">}</span>):
<span class="n">Promise</span><<span class="n">string</span>>, <span class="c">// unified diff text</span>
<span class="n">show</span>(pid, sha: <span class="n">string</span>): <span class="n">Promise</span><Commit + diff>,
<span class="n">listTags</span>(pid): <span class="n">Promise</span><Tag[]>,
<span class="c">// 写回滚 (P-8 recovery 用)</span>
<span class="n">resetWorkingTree</span>(pid, ref = <span class="s">'HEAD'</span>): <span class="n">Promise</span><<span class="p">{</span> ok <span class="p">}</span>>, <span class="c">// 抛弃未 commit 的脏文件</span>
<span class="n">revert</span>(pid, sha: <span class="n">string</span>): <span class="n">Promise</span><<span class="p">{</span> ok, newSha <span class="p">}</span>>, <span class="c">// 反向应用</span>
<span class="c">// 导出</span>
<span class="n">archive</span>(pid, ref = <span class="s">'HEAD'</span>, format = <span class="s">'zip'</span>):
<span class="n">Promise</span><<span class="p">{</span> path: <span class="n">string</span> <span class="p">}</span>>, <span class="c">// 跑 git archive</span>
<span class="c">// v2 留口</span>
<span class="n">addRemote</span>?(pid, name, url),
<span class="n">push</span>?(pid, remote = <span class="s">'origin'</span>),
<span class="p">}</span>;</pre>
<ul class="contract">
<li>实现可走 simple-git 包 或 spawn git;推荐 <b>spawn git</b>(零依赖,CLAUDE.md 强约束的 JSON 也不涉及)</li>
<li>所有方法<b>串行化 per-pid</b>(避免同项目并发 git 操作打架)</li>
<li>commit author = Agent 名("PM Agent <pm@devloop.local>")便于 git log 看哪个 Agent 干的</li>
<li>失败处理:git 子命令非零退出 → 抛错;nothing-to-commit 默认 skipEmpty=false 返 skipped=true,不抛</li>
</ul>
<ul class="tests">
<li>init 后 .git 存在 + .gitignore 已 commit</li>
<li>commit({kind:'prd'}) 后 git log 有该 commit + author 是 PM Agent</li>
<li>nothing-to-commit 不抛错</li>
<li>tag('shipped-v1') 后 git tag --list 含</li>
<li>diff(sha1, sha2) 返回非空 unified diff</li>
</ul>
</section>
<section data-def-name="Process.GitCommit" data-mvcp="process">
<h2>⑧ P-10 · 自动提交流程</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">// 在 P-8 callAgentWithPersistence success 分支末尾添加</span>
<span class="c">// (LoopController 调,Agent 完全无感)</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">// 某些 agent 不 commit (如 Integrator 失败时)</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="k">function</span> <span class="n">buildCommitSpec</span>(name, result, input) <span class="p">{</span>
<span class="k">if</span> (name === <span class="s">'pm'</span>) <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
kind: <span class="s">'prd'</span>,
summary: <span class="s">`prd: rev </span><span class="v">${result.prd_rev}</span><span class="s"> · </span><span class="v">${result.change_summary}</span><span class="s">`</span>,
author: <span class="p">{</span> name: <span class="s">'PM Agent'</span>, email: <span class="s">'pm@devloop.local'</span> <span class="p">}</span>,
<span class="p">}</span>;
<span class="p">}</span>
<span class="k">if</span> (name === <span class="s">'architect'</span>) <span class="p">{</span>
<span class="k">const</span> prefix = input.mode === <span class="s">'patch'</span> ? <span class="s">'sdd: patch'</span> : <span class="s">'sdd: rev '</span> + result.sdd_rev;
<span class="k">return</span> <span class="p">{</span>
kind: <span class="s">'sdd'</span>,
summary: <span class="s">`</span><span class="v">${prefix}</span><span class="s"> · </span><span class="v">${result.units_count}</span><span class="s"> units · </span><span class="v">${result.change_summary}</span><span class="s">`</span>,
author: <span class="p">{</span> name: <span class="s">'Architect Agent'</span>, email: <span class="s">'architect@devloop.local'</span> <span class="p">}</span>,
<span class="p">}</span>;
<span class="p">}</span>
<span class="k">if</span> (name === <span class="s">'coder'</span> || name === <span class="s">'tester'</span>) <span class="p">{</span>
<span class="c">// Coder/Tester 合并 commit:Tester 绿后才 commit,把 code + tests 一起</span>
<span class="k">if</span> (name === <span class="s">'coder'</span>) <span class="k">return</span> <span class="v">null</span>; <span class="c">// 等 Tester 绿</span>
<span class="k">if</span> (!result.ok) <span class="k">return</span> <span class="v">null</span>; <span class="c">// Tester 红不 commit</span>
<span class="k">const</span> verb = result.attempt > <span class="v">1</span> ? <span class="s">'retry → green'</span> : <span class="s">'implement'</span>;
<span class="k">return</span> <span class="p">{</span>
kind: <span class="s">'code'</span>, unit_id: result.unit_id,
summary: <span class="s">`code(</span><span class="v">${result.unit_id}</span><span class="s">): </span><span class="v">${verb}</span><span class="s"> · attempt </span><span class="v">${result.attempt}</span><span class="s">`</span>,
author: <span class="p">{</span> name: <span class="s">'Coder Agent'</span>, email: <span class="s">'coder@devloop.local'</span> <span class="p">}</span>,
<span class="p">}</span>;
<span class="p">}</span>
<span class="k">if</span> (name === <span class="s">'integrator'</span>) <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
kind: <span class="s">'integ'</span>, run_id: result.run_id,
summary: <span class="s">`integ(</span><span class="v">${result.run_id.slice(-12)}</span><span class="s">): </span><span class="v">${result.ok ? 'pass' : 'fail'}</span><span class="s"> · </span><span class="v">${result.cases_passed}</span><span class="s">/</span><span class="v">${result.cases_total}</span><span class="s">`</span>,
author: <span class="p">{</span> name: <span class="s">'Integrator Agent'</span>, email: <span class="s">'integrator@devloop.local'</span> <span class="p">}</span>,
<span class="p">}</span>;
<span class="p">}</span>
<span class="k">if</span> (name === <span class="s">'bug-triager'</span>) <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
kind: <span class="s">'triage'</span>, run_id: input.run_id,
summary: <span class="s">`triage(</span><span class="v">${input.run_id.slice(-12)}</span><span class="s">): </span><span class="v">${result.category}</span><span class="s"> · confidence </span><span class="v">${result.confidence}</span><span class="s">`</span>,
author: <span class="p">{</span> name: <span class="s">'BugTriager Agent'</span>, email: <span class="s">'triager@devloop.local'</span> <span class="p">}</span>,
<span class="p">}</span>;
<span class="p">}</span>
<span class="k">return</span> <span class="v">null</span>;
<span class="p">}</span></pre>
<ul class="contract">
<li>Coder commit 推迟到 Tester 绿(避免红色代码进 main)</li>
<li>所有 commit author 标 Agent 名,git log 可读「哪个 Agent 改了哪段」</li>
<li>nothing-to-commit 是 ok(某些 retry 没改文件)</li>
<li>commit 失败 → 上报 LoopController · 不阻塞 Agent 返回(git 是 nice-to-have,不是阻塞依赖)</li>
</ul>
<h3>项目级 tag 时机</h3>
<table class="dense">
<thead><tr><th>事件</th><th>tag 名</th><th>触发</th></tr></thead>
<tbody>
<tr><td>L2 通过</td><td><code>shipped-v{prd_rev}-{sdd_rev}</code></td><td>LoopController.runProject return 'shipped'</td></tr>
<tr><td>人类 sign-off</td><td><code>final-v{prd_rev}</code></td><td>POST /sign-off</td></tr>
<tr><td>abandon</td><td><code>abandoned</code></td><td>POST /abandon</td></tr>
<tr><td>每个 PRD rev</td><td><code>prd-rev-{N}</code></td><td>PM 完成且写完 changelog</td></tr>
<tr><td>每个 SDD rev</td><td><code>sdd-rev-{N}</code></td><td>Architect 完成 generate(不含 patch)</td></tr>
</tbody>
</table>
</section>
<section>
<h2>⑨ 与 timeline / changelog / bugs 的关系</h2>
<div class="grid-3">
<div class="card">
<h4>git log</h4>
<p>真源:<b>文件层变化</b>。git diff 看代码 / 文档真实改了什么。<br/>包括 author / sha / 时间。</p>
</div>
<div class="card">
<h4>timeline.ndjson</h4>
<p>真源:<b>事件流</b>。含非文件改动(如 SSE / Agent 进出 / state transition)。比 git log 信息量更大。</p>
</div>
<div class="card">
<h4>changelog.ndjson</h4>
<p>派生:<b>文档级摘要</b>。每条对应一次 PRD/SDD/units rev,与 git tag <code>prd-rev-N</code> / <code>sdd-rev-N</code> 一一对应。</p>
</div>
</div>
<p>三者互补不冲突。git log 和 timeline 各自<b>append-only</b>,drift 风险低(都由 LoopController 在 callAgent 末尾原子写)。</p>
</section>
<section>
<h2>⑩ 失败恢复 · 新能力</h2>
<table>
<thead><tr><th>场景</th><th>没 git 时</th><th>有 git 时</th></tr></thead>
<tbody>
<tr><td>Server crash</td><td>agent-state.paused,但代码可能<b>半写</b></td><td><code>git status</code> 看脏文件 · <code>git reset --hard HEAD</code> 一键恢复</td></tr>
<tr><td>Coder 3 次 retry 都红</td><td>只能看 coder.log 文字</td><td><code>git log --author='Coder'</code> 看 3 次 commit · <code>git diff</code> 看模型 3 次都改了啥</td></tr>
<tr><td>L2 通过后人类突然改主意</td><td>整个手动 hack 回退</td><td><code>git revert ship-v1</code> 即可</td></tr>
<tr><td>L3 反馈引入回归</td><td>需要重新跑全套</td><td><code>git bisect</code> 找到哪次 commit 引入了破坏</td></tr>
<tr><td>项目导出</td><td><code>cp -r</code> 整个目录 30MB</td><td><code>git archive HEAD</code> 干净打包,仅 tracked 文件 ~5MB</td></tr>
<tr><td>跨机器同步项目状态</td><td>不可能(带 .vitest-cache 等垃圾)</td><td><code>git push</code> + 对端 <code>git clone</code></td></tr>
</tbody>
</table>
</section>
<section>
<h2>⑪ P-8 状态机的协同</h2>
<p>P-8 持久化层与 git 不重叠:</p>
<pre class="code shape"><span class="c">// Agent 调用结束的完整流程 (P-8 + P-10)</span>
<span class="k">async function</span> <span class="n">callAgentWithPersistence</span>(name, input) <span class="p">{</span>
<span class="c">// P-8 step 1: 写 runtime status=running</span>
<span class="k">await</span> <span class="n">writeRuntimeState</span>(pid, name, <span class="s">'running'</span>);
<span class="c">// 实际调用 (Agent 通过工具写文件)</span>
<span class="k">const</span> result = <span class="k">await</span> agentManager.<span class="n">callAgent</span>(...);
<span class="c">// P-8 step 2: 写 runtime status=done</span>
<span class="k">await</span> <span class="n">writeRuntimeState</span>(pid, name, <span class="s">'done'</span>);
<span class="c">// P-10 step 3: git commit (新增)</span>
<span class="k">if</span> (result.ok) <span class="k">await</span> <span class="n">commitAfterAgent</span>(pid, name, result, input);
<span class="c">// timeline / SSE / changelog 等照旧</span>
...
<span class="p">}</span>
<span class="c">// recoverOnBoot 增加 git 清理:</span>
<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">// 既有: 扫 agent-state · 改 running → paused</span>
...
<span class="c">// 新增: 检查 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>
<span class="c">// 上次 crash 留下未提交脏文件</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>
</section>
<section>
<h2>⑫ 推 remote · v2 留口</h2>
<p>v1 不做。但 GitVault API 已留 <code>addRemote</code> + <code>push</code>。v2 可加:</p>
<pre class="code"><span class="c"># 把项目代码推到 GitHub</span>
POST /api/devloop/projects/:pid/push
<span class="p">{</span>
<span class="n">"remote"</span>: <span class="s">"origin"</span>,
<span class="n">"url"</span>: <span class="s">"git@github.com:yibo/auto-todo.git"</span>,
<span class="n">"branch"</span>: <span class="s">"main"</span>,
<span class="n">"open_pr"</span>: <span class="v">false</span>
<span class="p">}</span>
<span class="c"># GitHub Actions 接管 CI/CD,DevLoop 只负责生产 + 提交</span></pre>
<p>v2 还可加 <b>git submodule 共享</b>:跨项目复用 utils → 抽 shared/ submodule。</p>
</section>
<section>
<h2>⑬ 安全考量</h2>
<table class="dense">
<thead><tr><th>风险</th><th>缓解</th></tr></thead>
<tbody>
<tr><td>git push 凭据泄露(v2)</td><td>v1 不实现 push;v2 用 SSH key per company,不存 password</td></tr>
<tr><td>Agent 越权调 git</td><td>Agent 的 toolPolicy 不含 git 工具,Bash 不允许 <code>git *</code> 命令(jsonGuard 类似的 cmdGuard)</td></tr>
<tr><td>commit 含敏感数据</td><td>项目 .gitignore 排除 secrets/ · token.txt · *.env</td></tr>
<tr><td>仓库膨胀</td><td>stream.log 大文件 .gitignore;定期 <code>git gc</code></td></tr>
<tr><td>并发 commit race</td><td>GitVault 按 pid 串行化(mutex per pid)</td></tr>
</tbody>
</table>
</section>
<section>
<h2>⑭ P0 工时增量</h2>
<table class="dense">
<thead><tr><th>P0 子项</th><th>新增工时</th></tr></thead>
<tbody>
<tr><td>GitVault 实现 (~150 行 spawn git wrapper)</td><td>1 天</td></tr>
<tr><td>.gitignore 模板 + LoopController.createProject git init 集成</td><td>0.5 天</td></tr>
<tr><td>commitAfterAgent + buildCommitSpec</td><td>0.5 天</td></tr>
<tr><td>recoverOnBoot 加 git status 检查</td><td>0.5 天</td></tr>
<tr><td>验证: phase0-verify 加 git init + 一次 commit 验证</td><td>0.5 天</td></tr>
<tr><td><b>P0 +3 天</b>(总 P0 14 天)</td><td>—</td></tr>
</tbody>
</table>
<p class="sig">原 11 天 P0 → 14 天,总 9.5 周 → 10 周。代价相对收益(真版本管理)合理。</p>
</section>
<section>
<h2>⑮ 实战命令</h2>
<pre class="code"><span class="c"># P0 后验证 git 集成</span>
<span class="c"># 1. 创建项目应该自动 git init</span>
curl -X POST http://localhost:3010/api/devloop/projects -d @/tmp/create.json
ls data/companies/c-test/projects/p-XXXXXX/.git/
<span class="c"># 期望: HEAD config objects refs/ 等</span>
<span class="c"># 2. 跑完 PM 应该有 1 个 prd commit</span>
git -C data/companies/c-test/projects/p-XXXXXX log --oneline
<span class="c"># 期望: 至少 2 行,init + prd: rev 1</span>
<span class="c"># 3. 跑完 Architect</span>
<span class="c"># 期望: 多 1 行 sdd: rev 1</span>
<span class="c"># 4. 跑完 Coder→Tester 绿后</span>
<span class="c"># 期望: 多 1 行 code(todo-be): implement · attempt 1</span>
<span class="c"># 5. L2 通过后</span>
git -C ... log --oneline
<span class="c"># 期望: ship: prd@1 sdd@1</span>
git -C ... tag --list
<span class="c"># 期望: prd-rev-1, sdd-rev-1, shipped-v1-1</span>
<span class="c"># 6. retry 演示</span>
<span class="c"># 注入 bug 让 Coder retry 2 次再绿</span>
git -C ... log --author='Coder' --oneline
<span class="c"># 期望: 仅 1 个 commit (因为只在 Tester 绿才 commit)</span>
<span class="c"># 但 coder.log 有 2 条 attempt 记录</span>
<span class="c"># 7. 导出</span>
node -e <span class="s">"require('./server/services/git-vault').gitVault.archive('p-XXXXXX').then(r => console.log(r))"</span>
<span class="c"># 期望: /tmp/p-XXXXXX-HEAD.zip 含 tracked files only</span></pre>
</section>
<section>
<h2>⑯ 关联</h2>
<ul>
<li><a href="sdd-process.html">sdd-process</a> · P-10 自动提交(同此页 §⑧)</li>
<li><a href="sdd-control.html">sdd-control</a> · C-18 GitVault(同此页 §⑦)</li>
<li><a href="data-layout.html">data-layout</a> · .git/ 目录位置</li>
<li><a href="plan-migration.html">plan-migration</a> · P0 工时已含 GitVault</li>
<li><a href="plan-code-lifecycle.html">代码归宿</a> · v2 自动 push 是这一层的延伸</li>
</ul>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site">
<div><b>Section</b>05 · Architecture</div>
<div><b>Page</b>arch-git-versioning.html</div>
<div><b>Spec Rev</b>v1 · 2026-05-13</div>
</footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>