agentic-flow-kit 1.7.16 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +11 -7
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +9 -8
  5. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  6. package/modules/hosts/claude/commands/wf-review.md +1 -1
  7. package/modules/hosts/claude/commands/wf-test.md +1 -1
  8. package/modules/hosts/codex/commands/wf-build.md +1 -1
  9. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  10. package/modules/hosts/codex/commands/wf-new-task.md +9 -8
  11. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  12. package/modules/hosts/codex/commands/wf-review.md +1 -1
  13. package/modules/hosts/codex/commands/wf-test.md +1 -1
  14. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  15. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  16. package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
  17. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  18. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  19. package/modules/hosts/cursor/commands/wf-test.md +1 -1
  20. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  21. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  22. package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
  23. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  24. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  25. package/modules/hosts/opencode/commands/wf-test.md +1 -1
  26. package/modules/hosts/trae/commands/wf-build.md +1 -1
  27. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  28. package/modules/hosts/trae/commands/wf-new-task.md +9 -8
  29. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  30. package/modules/hosts/trae/commands/wf-review.md +1 -1
  31. package/modules/hosts/trae/commands/wf-test.md +1 -1
  32. package/package.json +1 -1
  33. package/src/cli.mjs +6 -2
  34. package/src/doctor.mjs +11 -2
  35. package/src/executor.mjs +25 -8
  36. package/src/executor.test.mjs +4 -1
  37. package/src/fresh-init.test.mjs +1 -1
  38. package/src/init.mjs +20 -3
  39. package/src/profiles.mjs +7 -3
  40. package/src/rule-budget.test.mjs +2 -2
  41. package/src/stack-profile.test.mjs +6 -0
  42. package/src/validate.mjs +6 -1
  43. package/src/validate.test.mjs +1 -1
  44. package/templates/.gitattributes +4 -1
  45. package/templates/AGENTS.md +2 -2
  46. package/templates/_agents/commands/build.md +1 -1
  47. package/templates/_agents/commands/maintain.md +1 -1
  48. package/templates/_agents/commands/new-task.md +9 -8
  49. package/templates/_agents/commands/plan.md +1 -1
  50. package/templates/_agents/commands/review.md +1 -1
  51. package/templates/_agents/commands/test.md +1 -1
  52. package/templates/_agents/engine-lock.json +130 -42
  53. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  54. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  55. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  56. package/templates/_agents/lane-surfaces.txt +4 -1
  57. package/templates/_agents/level-hints.txt +1 -1
  58. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  59. package/templates/_agents/mechanism-registry.md +27 -0
  60. package/templates/_agents/rule-budgets.txt +2 -2
  61. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  62. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  63. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  64. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  65. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  66. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  67. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  68. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  69. package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
  70. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  71. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  72. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  73. package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
  74. package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
  75. package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
  76. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
  77. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
  78. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  79. package/templates/_agents/scripts/check-loop.mjs +215 -34
  80. package/templates/_agents/scripts/check-loop.test.mjs +570 -49
  81. package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
  82. package/templates/_agents/scripts/check-registry.mjs +45 -0
  83. package/templates/_agents/scripts/check-scope.mjs +96 -0
  84. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  85. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  86. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  87. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  88. package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
  89. package/templates/_agents/scripts/confirm-doc.mjs +114 -38
  90. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  91. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  92. package/templates/_agents/scripts/fill-intent.mjs +5 -3
  93. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  94. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  95. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  96. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  97. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  98. package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
  99. package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
  100. package/templates/_agents/scripts/gate-checklist.mjs +6 -0
  101. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  102. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  103. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  104. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  105. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  106. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
  107. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  108. package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
  109. package/templates/_agents/scripts/git-core.mjs +45 -0
  110. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  111. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  112. package/templates/_agents/scripts/kb-search.mjs +19 -4
  113. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  114. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  115. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  116. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  117. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  118. package/templates/_agents/scripts/level-criteria.mjs +9 -10
  119. package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
  120. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  121. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  122. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  123. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  124. package/templates/_agents/scripts/pipeline-run.mjs +6 -22
  125. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  126. package/templates/_agents/scripts/policy.mjs +28 -0
  127. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  128. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  129. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  130. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  131. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  132. package/templates/_agents/scripts/run-tests.mjs +99 -0
  133. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  134. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  135. package/templates/_agents/scripts/stage-gates.mjs +34 -12
  136. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  137. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  138. package/templates/_agents/scripts/verify.mjs +135 -58
  139. package/templates/_agents/scripts/verify.test.mjs +156 -47
  140. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  141. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  142. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  143. package/templates/_agents/scripts-test-exempt.txt +1 -0
  144. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  145. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  146. package/templates/_agents/workflow-enums.txt +8 -1
  147. package/templates/_githooks/pre-commit +110 -90
  148. package/templates/_githooks/pre-merge-commit +27 -23
  149. package/templates/_githooks/pre-push +4 -3
  150. package/templates/_github/workflows/kit-ci.yml +4 -3
  151. package/templates/workflow/README.md +14 -7
  152. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  153. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  154. package/templates/workflow/papercuts.md +2 -0
  155. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  156. package/templates/workflow/regression-checklist.md +1 -0
  157. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  158. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  159. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  160. package/templates/_agents/scripts/rule-budget.sh +0 -74
@@ -47,7 +47,7 @@ console.log('✅ intent 草稿:' + out);
47
47
  fs.writeFileSync(out, body);
48
48
  console.log('✅ spec 草稿:' + out);
49
49
  `);
50
- fake('fill-plan.mjs', FILL_HEAD + `const body = '---\\n状态: draft\\n级别: ' + (lv === 'L0' ? 'L1' : lv) + '\\n模块: pipeline\\n---\\n# PLAN — ' + t + '\\n\\n## 改动方案\\n\\n- src/app.mjs:实现功能\\n\\n## 约束与风险\\n\\n- 风险低\\n\\n## 任务拆解\\n\\n1. 实现\\n\\n## 执行顺序\\n\\n1\\n\\n## 验证计划\\n\\n- 静态门:npm test\\n\\n## 确认与复核\\n- 待\\n';
50
+ fake('fill-plan.mjs', FILL_HEAD + `const body = '---\\n状态: draft\\n级别: ' + lv + '\\n模块: pipeline\\n---\\n# PLAN — ' + t + '\\n\\n## 改动方案\\n\\n- src/app.mjs:实现功能\\n\\n## 约束与风险\\n\\n- 风险低\\n\\n## 任务拆解\\n\\n1. 实现\\n\\n## 执行顺序\\n\\n1\\n\\n## 验证计划\\n\\n- 静态门:npm test\\n\\n## 确认与复核\\n- 待\\n';
51
51
  fs.writeFileSync(out, body);
52
52
  console.log('✅ plan 草稿:' + out);
53
53
  `);
@@ -289,22 +289,13 @@ test('修复环 cap 3 → 超限 gate-fail exit 2', () => {
289
289
  assert.match(out(p), /修复环超限/);
290
290
  });
291
291
 
292
- test('L0 路径:直改 → 提交 → done', () => {
293
- const f = buildFixture();
294
- prun(f, ['start', '改一处文案']);
295
- prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=copy1']);
296
- const pw = prun(f, ['next']); // 无 --files:重发工单
297
- assert.match(out(pw), /PIPELINE-STOP work-order/);
298
- const p = prun(f, ['next', '--files', 'README.md'], { FIXTURE_GIT_STATUS: ' M README.md' });
299
- assert.equal(p.status, 0, out(p));
300
- assert.match(out(p), /PIPELINE-STOP done/);
301
- assert.match(fs.readFileSync(f.gitLog, 'utf8'), /commit -m docs\(wiki\): 改一处文案/);
302
- });
292
+ // (2026-10-13 L0 退役,自源仓收敛系列 1 回灌:原「L0 路径:直改 → 提交 → done」场景随 direct-edit
293
+ // 分支删除——最低档 L1 走 intent+plan 闭环,无直改通道。)
303
294
 
304
295
  test('verify-only 短路径直达 done', () => {
305
296
  const f = buildFixture();
306
297
  prun(f, ['start', '跑下 verify']);
307
- const p = prun(f, ['next', '--triage', 'kind=verify level=L0 module=pipeline topic=v1']);
298
+ const p = prun(f, ['next', '--triage', 'kind=verify level=L1 module=pipeline topic=v1']);
308
299
  assert.equal(p.status, 0, out(p));
309
300
  assert.match(out(p), /PIPELINE-STOP done/);
310
301
  assert.ok(runFile(f, latestId(f)).events.some((e) => e.type === 'gate' && e.cmd.includes('verify.mjs') && e.exit === 0));
@@ -313,7 +304,7 @@ test('verify-only 短路径直达 done', () => {
313
304
  test('review-only:工单 → --done 收尾可达(复核 P1-1 回归)', () => {
314
305
  const f = buildFixture();
315
306
  prun(f, ['start', '评审一下']);
316
- const pw = prun(f, ['next', '--triage', 'kind=review level=L0 module=pipeline topic=rv1']);
307
+ const pw = prun(f, ['next', '--triage', 'kind=review level=L1 module=pipeline topic=rv1']);
317
308
  assert.match(out(pw), /PIPELINE-STOP work-order/);
318
309
  const pd = prun(f, ['next', '--done']);
319
310
  assert.equal(pd.status, 0, out(pd));
@@ -332,7 +323,7 @@ test('deploy 路径:三件 done → prep 工单 → 机器校验 → 授权
332
323
  const f = buildFixture();
333
324
  seedDeployDocs(f, { intentState: 'done', planState: 'done' });
334
325
  prun(f, ['start', '可以上了']);
335
- const pw = prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=dep1']);
326
+ const pw = prun(f, ['next', '--triage', 'kind=deploy level=L1 module=pipeline topic=dep1']);
336
327
  assert.equal(pw.status, 0, out(pw));
337
328
  assert.match(out(pw), /deploy-prep/);
338
329
  assert.match(out(pw), /PIPELINE-STOP work-order/);
@@ -350,7 +341,7 @@ test('deploy 三件缺口 → gate-fail 列缺口 exit 2(P1-2)', () => {
350
341
  const f = buildFixture();
351
342
  seedDeployDocs(f, { intentState: 'done', planState: 'approved' }); // plan 未关单
352
343
  prun(f, ['start', '可以上了']);
353
- prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=dep1']);
344
+ prun(f, ['next', '--triage', 'kind=deploy level=L1 module=pipeline topic=dep1']);
354
345
  const p = prun(f, ['next']);
355
346
  assert.equal(p.status, 2);
356
347
  assert.match(out(p), /须 done/);
@@ -362,7 +353,7 @@ test('deploy 边界:incident closed 但 plan 缺失 → gate-fail(复核 P2-
362
353
  fs.writeFileSync(path.join(f.root, 'workflow', 'incidents', '2026-10-02-incd.md'),
363
354
  '---\n状态: closed\n发现: 2026-10-02\n模块: pipeline\n---\n# INCIDENT — incd\n\n## 时间线\n\n- x\n\n## 根因\n\nx\n\n## 复盘三件套\n\nx\n');
364
355
  prun(f, ['start', '可以上了']);
365
- prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=incd']);
356
+ prun(f, ['next', '--triage', 'kind=deploy level=L1 module=pipeline topic=incd']);
366
357
  const p = prun(f, ['next']);
367
358
  assert.equal(p.status, 2);
368
359
  assert.match(out(p), /plans\/2026-10-02-incd\.md(缺失——须 done)/);
@@ -372,32 +363,26 @@ test('deploy 边界:topic 子串不再误匹配(复核 P2-C 收紧回归)'
372
363
  const f = buildFixture();
373
364
  seedDeployDocs(f, { intentState: 'done', planState: 'done', topic: 'dep1' });
374
365
  prun(f, ['start', '可以上了']);
375
- const p = prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=dep']); // 旧版 includes 会误命中 dep1
366
+ const p = prun(f, ['next', '--triage', 'kind=deploy level=L1 module=pipeline topic=dep']); // 旧版 includes 会误命中 dep1
376
367
  assert.equal(p.status, 2);
377
368
  assert.match(out(p), /找不到可上线的对象/);
378
369
  });
379
370
 
380
371
  // P2-4 A′:提交命中 managed 面的处理(装户降级 WARN / 包源自愈 sync)
372
+ // (2026-10-13 L0 退役:原用例经 direct-edit 直改通道驱动;该通道随级别退役删除,改由 L1 闭环的
373
+ // workflow 文档提交(plan-confirm 批次一 docsCommit)驱动同一 managedFaceCheck 判定面。)
381
374
  test('P2-4 A′:命中台账件且无 flow-kit → 装户降级 WARN + 台账随提交(复核回归)', () => {
382
375
  const f = buildFixture();
383
- fs.mkdirSync(path.join(f.root, '.agents', 'notes'), { recursive: true });
384
- fs.writeFileSync(path.join(f.root, '.agents', 'notes', 'runtime-env.md'), 'notes\n');
385
- fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: '.agents/notes/runtime-env.md', sha256: 'x' }] }));
386
- prun(f, ['start', '改一处文案']);
387
- prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=mng1']);
388
- editDoc(f, '.agents/notes/runtime-env.md', (t) => t + '\n- 演练追加一行\n');
389
- const p = prun(f, ['next', '--files', '.agents/notes/runtime-env.md'], { FIXTURE_GIT_STATUS: ' M .agents/notes/runtime-env.md' });
390
- assert.equal(p.status, 0, out(p));
391
- assert.match(out(p), /⚠️ 触及 managed 面/);
392
- const addLine = fs.readFileSync(f.gitLog, 'utf8').split('\n').find((l) => l.startsWith('add '));
393
- assert.equal(addLine, 'add .agents/notes/runtime-env.md', '装户降级只 WARN:add 面不含 kit.json(复核 P2-D 断言改实)');
376
+ fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: 'workflow/intents/2026-10-02-mng1.md', sha256: 'x' }] }));
377
+ driveToImplement(f, 'L1', 'mng1');
378
+ const run = runFile(f, latestId(f));
379
+ assert.ok(run.events.some((e) => e.type === 'warn' && /触及 managed 面/.test(e.note)), '装户环境降级 WARN 出账');
380
+ assert.doesNotMatch(fs.readFileSync(f.gitLog, 'utf8'), /add .*\.agents\/kit\.json/, '装户降级只 WARN:add 面不含 kit.json(复核 P2-D 断言改实)');
394
381
  });
395
382
 
396
383
  test('P2-4 A′:包源环境自动 sync——flow-kit 假件被调 + 产物入提交(复核回归)', () => {
397
384
  const f = buildFixture();
398
- fs.mkdirSync(path.join(f.root, '.agents', 'notes'), { recursive: true });
399
- fs.writeFileSync(path.join(f.root, '.agents', 'notes', 'runtime-env.md'), 'notes\n');
400
- fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: '.agents/notes/runtime-env.md', sha256: 'x' }] }));
385
+ fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: 'workflow/intents/2026-10-02-mng2.md', sha256: 'x' }] }));
401
386
  fs.mkdirSync(path.join(f.root, 'bin'), { recursive: true });
402
387
  const syncLog = path.join(f.root, 'flowkit-sync.log');
403
388
  fs.writeFileSync(path.join(f.root, 'bin', 'flow-kit.mjs'), `#!/usr/bin/env node
@@ -408,11 +393,7 @@ const kit = JSON.parse(fs.readFileSync(kp, 'utf8'));
408
393
  kit.managed[0].sha256 = 'refreshed';
409
394
  fs.writeFileSync(kp, JSON.stringify(kit));
410
395
  `);
411
- prun(f, ['start', '改一处文案']);
412
- prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=mng2']);
413
- editDoc(f, '.agents/notes/runtime-env.md', (t) => t + '\n- 演练追加一行\n');
414
- const p = prun(f, ['next', '--files', '.agents/notes/runtime-env.md'], { FIXTURE_GIT_STATUS: ' M .agents/notes/runtime-env.md' });
415
- assert.equal(p.status, 0, out(p));
396
+ driveToImplement(f, 'L1', 'mng2');
416
397
  assert.match(fs.readFileSync(syncLog, 'utf8'), /sync-called/);
417
398
  const run = runFile(f, latestId(f));
418
399
  assert.ok(run.events.some((e) => e.type === 'gate' && e.cmd.includes('flow-kit.mjs sync') && e.exit === 0));
@@ -423,11 +404,10 @@ fs.writeFileSync(kp, JSON.stringify(kit));
423
404
  test('P2-4 A′:未命中台账面 → 不触发 sync/告警', () => {
424
405
  const f = buildFixture();
425
406
  fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: '.agents/notes/runtime-env.md', sha256: 'x' }] }));
426
- prun(f, ['start', '改一处文案']);
427
- prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=mng3']);
428
- const p = prun(f, ['next', '--files', 'README.md'], { FIXTURE_GIT_STATUS: ' M README.md' });
429
- assert.equal(p.status, 0, out(p));
430
- assert.doesNotMatch(out(p), /managed 面/);
407
+ driveToImplement(f, 'L1', 'mng3');
408
+ const run = runFile(f, latestId(f));
409
+ assert.ok(!run.events.some((e) => e.type === 'warn' && /managed 面/.test(e.note)), '未命中台账面不出告警');
410
+ assert.ok(!run.events.some((e) => e.type === 'gate' && String(e.cmd).includes('flow-kit.mjs sync')), '未命中台账面不触发 sync');
431
411
  });
432
412
 
433
413
  test('rename 行 old+new 双收录防逃逸(复核 P2-2 回归)', () => {
@@ -149,6 +149,34 @@ export const POLICIES = {
149
149
  },
150
150
  };
151
151
 
152
+ // ---- 版本 9–12(2026-10-13 B6b-1,自源仓 policy v9–v12 对齐回灌)----
153
+ // 构造式沿源仓 2026-10-10 收敛系列 6 的 merge 链(基线 + 显式差异):kit v1–v8 键史原样保留不动,
154
+ // 新版本在 kit v8 之上做超集追加,消除「全量手抄日期键、漏抄即静默改语义」的脆弱点(v2 注释自警同源)。
155
+ // 键名与源仓逐字一致;kit 无源仓 v6–v8 的 check14HardSince / verifyHardSince——那两个键的消费段
156
+ // (检查 14 hard 档 / 凭证 hard 档)kit 仍为恒 advisory 面,未落地不引入。incidentRegrSince 随 B6b-2
157
+ //(§21–26 同批)落地并激活其消费段 §3x 防复发证据门。各锚日期 = kit 落地日口径:
158
+ // 发布当日(2026-10-13)= specVerifSince / attributionSince / convergenceSince(advisory 档沿源仓
159
+ // convergenceSince「发布当日」先例——当日起、存量不追溯);
160
+ // 发布次日(2026-10-14)= convergenceHardSince / reviewerSince / incidentRegrSince(沿源仓
161
+ // check14HardSince「发布次日」先例——当日在途件不追溯、维持 advisory 留痕,锚起新违例才升格)。
162
+ // 缺键(v1–v8)→ 对应消费段整体跳过(存量零噪声)。抄全键仍是铁律。
163
+ const merge = (base, over) => ({ ...base, ...over });
164
+ const V9 = merge(POLICIES[8], {
165
+ incidentRegrSince: '2026-10-14', // 检查 3x incident 防复发验证证据门生效锚(B6b-2 §3x 消费,hard 档「发布次日」口径——在途 fixed 件不追溯);v1–v8 无此键 → 该段整体跳过
166
+ specVerifSince: '2026-10-13', // 检查 21 spec 验证落地核查生效锚(B6b-2 §21 消费;kit 落地 2026-10-13);v1–v8 无此键 → 该检查整体跳过
167
+ attributionSince: '2026-10-13', // 检查 24 incident 归因字段生效锚(B6b-2 §24 消费;kit 落地 2026-10-13);v1–v8 无此键 → 该检查整体跳过
168
+ });
169
+ const V10 = merge(V9, {
170
+ convergenceSince: '2026-10-13', // 检查 23 判据级收敛对账生效锚(B6b-2 §23 advisory 档消费;kit 落地 2026-10-13);v1–v9 无此键 → 该检查整体跳过
171
+ });
172
+ const V11 = merge(V10, {
173
+ convergenceHardSince: '2026-10-14', // 检查 23 证据无效 hard 锚(B6b-2 §23 hard 档消费;kit 落地 2026-10-14);v1–v10 无此键 → 维持 warning
174
+ });
175
+ const V12 = merge(V11, {
176
+ reviewerSince: '2026-10-14', // 检查 1 独立复核上下文标识生效锚(B6b-2 检查 1 扩展消费;kit 落地 2026-10-14);v1–v11 无此键 → 该子检查整体跳过
177
+ });
178
+ Object.assign(POLICIES, { 9: V9, 10: V10, 11: V11, 12: V12 });
179
+
152
180
  export function loadKitPolicy(root) {
153
181
  let raw = null;
154
182
  try {
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ // resolve-merge-conflicts.mjs — 台账与派生生成物合并冲突一键自愈器(2026-10-10;B9 自 Shipyard.Material
3
+ // 回灌——判定面逐字对齐,出处沿革留痕在包源仓 git 历史与 CHANGELOG,装户径不引实例路径)
4
+ // 解决什么:多人并发提同一分支,或单人多 worktree 并行后合并时,派生生成物与 append-only 台账走
5
+ // git 文本合并产生冲突。第一道防线是 `.gitattributes` 的 `merge=jsonl-union` 驱动(台账类冲突直接
6
+ // 确定性并集,见 git-merge-jsonl.mjs);本脚本是**兜底**——driver / gitattributes 行漏配退化成文本
7
+ // 冲突、或 INDEX.md 这类生成物仍要人工解时,一键消解派生面,把人留在业务冲突上。
8
+ // 逻辑:
9
+ // 1. 扫描当前 Git 处于 unmerged (UU / AA / DD / AU / UA 等) 状态的文件;
10
+ // 2. 若 workflow/INDEX.md 冲突:checkout --ours 后重新调用 gen-workflow-index.mjs 全量生成并 git add;
11
+ // 3. 若 .agents/ 下 *.jsonl 台账冲突且双方内容都在(UU/AA):利用 git-merge-jsonl.mjs 合并当前与对方
12
+ // 版本并 git add。判定面按「.agents/ 前缀 + .jsonl 后缀」匹配,**天然含 confirmations.d/ 分片**——
13
+ // 分片是 kit 确认行的默认写入口径(2026-10-13 ledger-shard-default),分片 UU/AA 同走并集;
14
+ // 删除/改删型(DD/AU/UD/UA/DU)不做任何写入——自动并集会复活被删文件或凭空建空文件,仅提示人工裁决;
15
+ // 4. 输出处理清单,退出码 0。
16
+
17
+ import { execFileSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import { mergeJsonlLines } from './git-merge-jsonl.mjs';
20
+
21
+ const git = (args) => {
22
+ try { return execFileSync('git', args, { encoding: 'utf8' }).trim(); }
23
+ catch (e) { return ''; }
24
+ };
25
+
26
+ export function getUnmergedFiles() {
27
+ const status = git(['status', '--porcelain']);
28
+ if (!status) return [];
29
+
30
+ const unmerged = [];
31
+ for (const line of status.split(/\r?\n/)) {
32
+ if (!line) continue;
33
+ const code = line.slice(0, 2);
34
+ const file = line.slice(3).trim();
35
+ // 冲突代码:UU (both modified), AA (both added), DU, UD 等
36
+ if (/^(UU|AA|DD|AU|UD|UA|DU)/.test(code)) {
37
+ unmerged.push({ file, code });
38
+ }
39
+ }
40
+ return unmerged;
41
+ }
42
+
43
+ export function autoResolveConflicts() {
44
+ const files = getUnmergedFiles();
45
+ if (files.length === 0) {
46
+ console.log('[resolve-conflicts] 当前无未解决的合并冲突。');
47
+ return { resolved: [], remaining: [] };
48
+ }
49
+
50
+ const resolved = [];
51
+ const remaining = [];
52
+
53
+ for (const { file, code } of files) {
54
+ const normFile = file.replace(/\\/g, '/');
55
+
56
+ // 1. workflow/INDEX.md 派生索引冲突:取当前版本为底,重新全量生成
57
+ if (normFile === 'workflow/INDEX.md') {
58
+ try {
59
+ execFileSync('git', ['checkout', '--ours', 'workflow/INDEX.md'], { stdio: 'pipe' });
60
+ // 调用索引生成器重新渲染
61
+ execFileSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { stdio: 'pipe' });
62
+ execFileSync('git', ['add', 'workflow/INDEX.md'], { stdio: 'pipe' });
63
+ resolved.push('workflow/INDEX.md (已自动全量重生成)');
64
+ } catch (err) {
65
+ remaining.push(`workflow/INDEX.md: 重生成失败 (${err.message})`);
66
+ }
67
+ continue;
68
+ }
69
+
70
+ // 2. .agents/ 下 *.jsonl 台账冲突(主台账与 confirmations.d/ 分片同判定面):仅 UU/AA(双方内容
71
+ // 都在)取当前版本和对方版本合并去重;删除/改删型(DD/AU/UD/UA/DU)不可并集,自动写入会
72
+ // 复活被删文件或凭空建空文件
73
+ if (normFile.startsWith('.agents/') && normFile.endsWith('.jsonl')) {
74
+ if (!/^(UU|AA)$/.test(code)) {
75
+ remaining.push(`${file}: 删除/改删型冲突不自动处理,请人工裁决`);
76
+ continue;
77
+ }
78
+ try {
79
+ const ours = git(['show', `:2:${normFile}`]);
80
+ const theirs = git(['show', `:3:${normFile}`]);
81
+ const merged = mergeJsonlLines(ours, theirs);
82
+ fs.writeFileSync(file, merged, 'utf8');
83
+ execFileSync('git', ['add', file], { stdio: 'pipe' });
84
+ resolved.push(`${file} (已自动并集去重)`);
85
+ } catch (err) {
86
+ remaining.push(`${file}: 台账合并失败 (${err.message})`);
87
+ }
88
+ continue;
89
+ }
90
+
91
+ // 其余业务文件保持冲突,需人工审查
92
+ remaining.push(file);
93
+ }
94
+
95
+ return { resolved, remaining };
96
+ }
97
+
98
+ if (process.argv[1] && process.argv[1].endsWith('resolve-merge-conflicts.mjs')) {
99
+ const { resolved, remaining } = autoResolveConflicts();
100
+ if (resolved.length > 0) {
101
+ console.log('[resolve-conflicts] ✅ 已自动消解以下派生/台账冲突:');
102
+ resolved.forEach((f) => console.log(` - ${f}`));
103
+ }
104
+ if (remaining.length > 0) {
105
+ console.log('\n[resolve-conflicts] ⚠️ 剩余业务代码冲突需手动介入处理:');
106
+ remaining.forEach((f) => console.log(` - ${f}`));
107
+ } else if (resolved.length > 0) {
108
+ console.log('\n[resolve-conflicts] 所有台账冲突已全部清零,可继续 git commit 完成合并。');
109
+ }
110
+ }
@@ -0,0 +1,87 @@
1
+ // resolve-merge-conflicts.test.mjs — 端到端测试合并冲突自愈器的收窄行为(临时 git 仓库夹具;B9 自 Shipyard.Material 回灌):
2
+ // UU(双方内容都在)走自动并集并 git add;删除/改删型(UD 等)不自动写入、不 git add,仅提示人工裁决。
3
+ // 夹具说明:git merge 对「双方都删」会静默自动解决(不产生 DD 未合并条目,已实测),
4
+ // 故删除型冲突用 modify/delete 构造(ours 修改 + theirs 删除 → UD),确定性复现未合并条目,
5
+ // 且正是旧 bug 的复活路径(:3: 读取失败 → 吞错为 '' → 误写回 ours 内容并 git add)。
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ let pass = 0, fail = 0;
13
+ const check = (name, cond, detail = '') => {
14
+ if (cond) { pass++; console.log(`PASS ${name}`); }
15
+ else { fail++; console.log(`FAIL ${name}${detail ? ` —— ${detail}` : ''}`); }
16
+ };
17
+
18
+ const scriptPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'resolve-merge-conflicts.mjs');
19
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'resolve-conflicts-test-'));
20
+
21
+ const git = (...args) => spawnSync('git', args, { cwd: tmp, encoding: 'utf8' });
22
+ const porcelain = () => git('status', '--porcelain').stdout.split(/\r?\n/).filter(Boolean);
23
+
24
+ try {
25
+ // 夹具:临时 git 仓库,基线含主台账 + 分片
26
+ git('init');
27
+ git('config', 'user.email', 'test@local');
28
+ git('config', 'user.name', 'test');
29
+ git('config', 'core.autocrlf', 'false');
30
+ const ledger = path.join(tmp, '.agents', 'confirmations.jsonl');
31
+ const shard = path.join(tmp, '.agents', 'confirmations.d', 'shard-x.jsonl');
32
+ fs.mkdirSync(path.dirname(shard), { recursive: true });
33
+ fs.writeFileSync(ledger, '{"id":1}\n');
34
+ fs.writeFileSync(shard, '{"id":2}\n');
35
+ git('add', '-A');
36
+ git('commit', '-m', 'base');
37
+ const defaultBranch = git('rev-parse', '--abbrev-ref', 'HEAD').stdout.trim();
38
+
39
+ // side:改主台账(追加 id:3)+ 删分片
40
+ git('checkout', '-b', 'side');
41
+ fs.writeFileSync(ledger, '{"id":1}\n{"id":3}\n');
42
+ fs.rmSync(shard);
43
+ git('add', '-A');
44
+ git('commit', '-m', 'side');
45
+
46
+ // main:改主台账(追加 id:4,与 side 不同行 → UU)+ 改分片(side 已删 → UD)
47
+ git('checkout', defaultBranch);
48
+ fs.writeFileSync(ledger, '{"id":1}\n{"id":4}\n');
49
+ fs.writeFileSync(shard, '{"id":2}\n{"id":5}\n');
50
+ git('add', '-A');
51
+ git('commit', '-m', 'main');
52
+
53
+ const merge = git('merge', 'side');
54
+ const statusConflicted = porcelain();
55
+ check('S1 夹具产生 UU+UD 冲突(merge 失败且两条未合并条目就位)',
56
+ merge.status !== 0 &&
57
+ statusConflicted.includes('UU .agents/confirmations.jsonl') &&
58
+ statusConflicted.includes('UD .agents/confirmations.d/shard-x.jsonl'),
59
+ `merge-exit=${merge.status} status=${statusConflicted.join(' | ')}`);
60
+
61
+ const shardBefore = fs.readFileSync(shard, 'utf8');
62
+
63
+ // 被测 CLI
64
+ const proc = spawnSync(process.execPath, [scriptPath], { cwd: tmp, encoding: 'utf8' });
65
+
66
+ check('S2 CLI exit 0(remaining 非零也 exit 0 的既有契约)', proc.status === 0, `status=${proc.status} stderr=${proc.stderr}`);
67
+
68
+ const merged = fs.readFileSync(ledger, 'utf8');
69
+ check('S3 UU 主台账=双方行并集', merged === '{"id":1}\n{"id":4}\n{"id":3}\n', merged);
70
+ check('S4 UU 主台账无冲突标记', !merged.includes('<<<<<<<'), merged);
71
+ const statusAfter = porcelain();
72
+ const ledgerLine = statusAfter.find((l) => l.endsWith('.agents/confirmations.jsonl'));
73
+ check('S5 UU 主台账已 git add(不再未合并)', !!ledgerLine && !/^(UU|AA|DD|AU|UD|UA|DU)/.test(ledgerLine), ledgerLine || '(不在 status 中)');
74
+
75
+ check('S6 UD 分片未被 git add(仍处未合并态)',
76
+ statusAfter.includes('UD .agents/confirmations.d/shard-x.jsonl'),
77
+ statusAfter.join(' | '));
78
+ check('S7 UD 分片内容未被自动改写', fs.readFileSync(shard, 'utf8') === shardBefore, fs.readFileSync(shard, 'utf8'));
79
+ check('S8 stdout 提示分片删除/改删型冲突需人工裁决',
80
+ proc.stdout.includes('.agents/confirmations.d/shard-x.jsonl') && proc.stdout.includes('删除/改删型冲突不自动处理'),
81
+ proc.stdout);
82
+ } finally {
83
+ fs.rmSync(tmp, { recursive: true, force: true });
84
+ }
85
+
86
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
87
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // rewrite-file.mjs — 按行字面替换 + 响亮失败(2026-10-12 silent-noop-loud-failure)
3
+ // 背景(papercut 2026-10-06 行,留痕在包源仓 git 历史与 CHANGELOG;两次假落账实证):
4
+ // `node -e "fs.writeFileSync(p, s.replace(A, B))"` 对 **CRLF 检出文件**用 `\n` 锚串定位时
5
+ // **静默无操作**,脚本照样打印「落账 OK」——假成功比响亮失败危险得多(台账/文档被以为改了)。
6
+ // 本工具把该形态的三个失效点逐一堵死:
7
+ // ① 按行处理并保留原行尾(`/\r?\n/` 切分 + 检出 EOL 回写)——不再有「\n 锚串匹配不上 CRLF」这一类
8
+ // ② 替换数必须与 --expect 相符(默认 1)——0 命中即非零退出,**文件不写**,绝不存在「以为改了」
9
+ // ③ 写后回读校验(新锚已在、旧锚已零、行数不变、EOL 未变)——全过才打印成功;任一不过即非零退出
10
+ // 另防两类自伤:find === replace(无操作却报成功)与目标文件不存在(fail-loud,不静默建文件)。
11
+ // 原子写:同目录临时文件 + rename——中途崩溃不留半写文件(沿 git-merge-jsonl.mjs / compact-ledgers.mjs 惯例)。
12
+ // 用法:node .agents/scripts/rewrite-file.mjs <文件> --find <字面串> --replace <字面串>
13
+ // [--expect N] [--allow-multi] [--dry-run] [--root <仓库根>]
14
+ // --expect N 期望命中数(默认 1);实际 ≠ N → 非零退出且不写盘
15
+ // --allow-multi 显式声明允许多命中(等价 --expect 的宽松形态:N ≥ 1 即过,用于批量同串替换)
16
+ // --dry-run 只打印将发生的替换与校验预览,不写盘
17
+ // 测试:node .agents/scripts/rewrite-file.test.mjs
18
+ // 沿革:2026-10-12 立档(优先级 3:把「静默无操作」改成响亮失败)
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+
22
+ const argv = process.argv.slice(2);
23
+ const argOf = (k) => {
24
+ const i = argv.indexOf(k);
25
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
26
+ };
27
+ const DRY = argv.includes('--dry-run');
28
+ const MULTI = argv.includes('--allow-multi');
29
+ let file = '';
30
+ let find = null;
31
+ let replace = null;
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ if (a === '--find') find = argv[++i] ?? null;
35
+ else if (a === '--replace') replace = argv[++i] ?? null;
36
+ else if (a === '--expect') { /* 下面统一解析 */ }
37
+ else if (a === '--root') { /* 仅为与其它脚本口令一致,路径按 cwd 解析 */ i++; }
38
+ else if (a === '--dry-run' || a === '--allow-multi') { /* 旗标已处理 */ }
39
+ else if (!a.startsWith('--') && !file) file = a;
40
+ }
41
+ const expectRaw = argOf('--expect');
42
+
43
+ const USAGE = `用法:node .agents/scripts/rewrite-file.mjs <文件> --find <字面串> --replace <字面串> [--expect N] [--allow-multi] [--dry-run]`;
44
+ const die = (msg) => { console.error(`❌ rewrite-file: ${msg}\n${USAGE}`); process.exit(1); };
45
+ if (argv.includes('--help') || argv.includes('-h')) { console.log(USAGE); process.exit(0); }
46
+ if (!file) die('缺目标文件');
47
+ if (find === null || replace === null) die('--find 与 --replace 必须成对给出');
48
+ if (find === '') die('--find 不能为空串(空串会命中每一行,属误用)');
49
+ if (find === replace) die(`--find 与 --replace 相同(「${find.slice(0, 40)}」)——无操作却会报成功,正是本工具要堵的形态`);
50
+ let expect = 1;
51
+ if (expectRaw !== '') {
52
+ if (!/^\d+$/.test(expectRaw)) die(`--expect 须为非负整数(现 ${expectRaw})`);
53
+ expect = Number(expectRaw);
54
+ }
55
+ if (MULTI && expectRaw !== '') die('--allow-multi 与 --expect 互斥(一个宽松一个严格,同给无法裁决)');
56
+
57
+ const abs = path.resolve(process.cwd(), file);
58
+ if (!fs.existsSync(abs)) die(`目标文件不存在:${file}(不静默创建新文件——路径写错时应响亮失败)`);
59
+ let raw;
60
+ try { raw = fs.readFileSync(abs, 'utf8'); } catch (err) { die(`读取失败:${file}(${err.message})`); }
61
+
62
+ // ---- 按行处理 + EOL 保全 ----
63
+ const crlf = raw.includes('\r\n');
64
+ const eol = crlf ? '\r\n' : '\n';
65
+ const hadTrailingNewline = /\r?\n$/.test(raw);
66
+ const lines = raw.split(/\r?\n/);
67
+ // 末行是「 trailing newline 产生的空串」时不计入可替换行集合(避免把 EOF 空串当一行改)
68
+ const bodyLines = hadTrailingNewline ? lines.slice(0, -1) : lines;
69
+
70
+ let hits = 0;
71
+ const changedIdx = [];
72
+ const newBody = bodyLines.map((l, i) => {
73
+ if (!l.includes(find)) return l;
74
+ const n = l.split(find).length - 1;
75
+ hits += n;
76
+ changedIdx.push(i + 1);
77
+ return l.split(find).join(replace);
78
+ });
79
+
80
+ if (hits === 0) {
81
+ console.error(`❌ rewrite-file: 0 命中——未写盘(文件保持原样)`);
82
+ console.error(` 文件:${file}`);
83
+ console.error(` 行尾:${crlf ? 'CRLF(仓库 autocrlf 检出态)' : 'LF'}|总行数:${bodyLines.length}`);
84
+ console.error(` 锚串(前 80 字符):${JSON.stringify(find.slice(0, 80))}`);
85
+ console.error(` 常见成因:① 锚串与文件实际内容不一致(空格/全角/标点);② 用 \\n 跨行锚串去匹配 CRLF 文件——本工具按行匹配,跨行锚请拆成单行锚或改用 Edit 工具;③ 文件已被前一次改动写过。`);
86
+ process.exit(1);
87
+ }
88
+ if (MULTI) {
89
+ if (hits < 1) die('内部不变式违反:hits < 1');
90
+ } else if (hits !== expect) {
91
+ console.error(`❌ rewrite-file: 命中数 ${hits} ≠ 期望 ${expect}——未写盘(文件保持原样)`);
92
+ console.error(` 文件:${file}|命中行号:${changedIdx.join(', ')}`);
93
+ console.error(` 含义:锚串不是唯一命中——要么目标不唯一(应加 --expect ${hits} 或 --allow-multi 明示),要么文件已被改过。`);
94
+ process.exit(1);
95
+ }
96
+
97
+ const out = newBody.join(eol) + (hadTrailingNewline ? eol : '');
98
+ if (newBody.length !== bodyLines.length) die('内部不变式违反:行数发生变化');
99
+
100
+ if (DRY) {
101
+ console.log(`[dry-run] ${file}:将替换 ${hits} 处(行 ${changedIdx.join(', ')}),行尾 ${crlf ? 'CRLF' : 'LF'},未写盘`);
102
+ process.exit(0);
103
+ }
104
+
105
+ // ---- 原子写 ----
106
+ const tmp = `${abs}.rewrite-tmp`;
107
+ try {
108
+ fs.writeFileSync(tmp, out, 'utf8');
109
+ fs.renameSync(tmp, abs);
110
+ } catch (err) {
111
+ try { fs.unlinkSync(tmp); } catch { /* tmp 可能尚未创建 */ }
112
+ die(`写入失败:${file}(${err.message})——原文件应保持原样,请核对后重试`);
113
+ }
114
+
115
+ // ---- 写后回读校验(③):任一不过即非零退出,绝不打印成功 ----
116
+ const problems = [];
117
+ let back;
118
+ try { back = fs.readFileSync(abs, 'utf8'); } catch (err) { die(`回读失败:${file}(${err.message})——写入结果不可验证,请人工核对`); }
119
+ if (back !== out) problems.push('回读内容与拟写内容不一致(写入被截断或并发改动)');
120
+ if (back.includes(find)) problems.push('旧锚串仍存在于回读内容中(替换不完整)');
121
+ if (replace !== '' && !back.includes(replace)) problems.push('新锚串不在回读内容中');
122
+ const backLines = back.split(/\r?\n/);
123
+ const backBody = hadTrailingNewline ? backLines.slice(0, -1) : backLines;
124
+ if (backBody.length !== bodyLines.length) problems.push(`行数变化:${bodyLines.length} → ${backBody.length}`);
125
+ if (back.includes('\r\n') !== crlf) problems.push(`行尾形态变化:${crlf ? 'CRLF' : 'LF'} → ${back.includes('\r\n') ? 'CRLF' : 'LF'}`);
126
+ if (problems.length) {
127
+ console.error(`❌ rewrite-file: 写后回读校验未过——${file}`);
128
+ problems.forEach((p) => console.error(` - ${p}`));
129
+ console.error(' 现场已保留(不要手改),请核对后用 git 恢复或重新替换');
130
+ process.exit(1);
131
+ }
132
+
133
+ console.log(`✅ rewrite-file: ${file} 替换 ${hits} 处(行 ${changedIdx.join(', ')})|行尾 ${crlf ? 'CRLF' : 'LF'} 保全|行数 ${bodyLines.length} 不变|回读校验通过`);