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 &gt; 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 &lt;devloop@lemonboss.local&gt;"</span>
    initialBranch?: <span class="n">string</span>,            <span class="c">// 默认 "main"</span>
  <span class="p">}</span>): <span class="n">Promise</span>&lt;<span class="p">{</span> ok: <span class="n">boolean</span>, head: <span class="n">string</span> <span class="p">}</span>&gt;,

  <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>&lt;<span class="p">{</span> ok, sha?: <span class="n">string</span>, skipped?: <span class="n">boolean</span> <span class="p">}</span>&gt;,

  <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>&lt;<span class="p">{</span> ok, tag <span class="p">}</span>&gt;,

  <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>&lt;Commit[]&gt;,
  <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>&lt;<span class="n">string</span>&gt;,                    <span class="c">// unified diff text</span>
  <span class="n">show</span>(pid, sha: <span class="n">string</span>): <span class="n">Promise</span>&lt;Commit + diff&gt;,
  <span class="n">listTags</span>(pid): <span class="n">Promise</span>&lt;Tag[]&gt;,

  <span class="c">// 写回滚 (P-8 recovery 用)</span>
  <span class="n">resetWorkingTree</span>(pid, ref = <span class="s">'HEAD'</span>): <span class="n">Promise</span>&lt;<span class="p">{</span> ok <span class="p">}</span>&gt;,   <span class="c">// 抛弃未 commit 的脏文件</span>
  <span class="n">revert</span>(pid, sha: <span class="n">string</span>): <span class="n">Promise</span>&lt;<span class="p">{</span> ok, newSha <span class="p">}</span>&gt;,        <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>&lt;<span class="p">{</span> path: <span class="n">string</span> <span class="p">}</span>&gt;,      <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 &lt;pm@devloop.local&gt;")便于 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 &amp;&amp; !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 &gt; <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 =&gt; 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>