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
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // check-ledger-invariant.test.mjs — 台账提交不变量 fixture 套件(2026-10-02 ledger-ci-invariant)
3
3
  // 真 git 仓注入:合法追加放行 / 六类篡改与行级违例检出 / --staged 双态 / 首次入库与空台账静默。
4
+ // 2026-10-13 深水批次并集:分片 H 系 / 特许修剪 P 系 / 归档感知 A 系 / 合并并集与合并审计 M 系
5
+ // (M6 合并收口删片·首父盲区 / M7 借收口抹账 / M8 补登追认·语义身份)/ 历史基线 B1——
6
+ // 场景自实战实现同名套件移植,判据适配处见各场景注释。
4
7
  import fs from 'node:fs';
5
8
  import os from 'node:os';
6
9
  import path from 'node:path';
@@ -9,6 +12,7 @@ import { fileURLToPath } from 'node:url';
9
12
 
10
13
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
11
14
  const SCRIPT = path.join(SCRIPT_DIR, 'check-ledger-invariant.mjs');
15
+ const PRUNE_REL = '.agents/ledger-prune-manifest.json'; // 与脚本单源(ledger-prune.mjs)同路径
12
16
 
13
17
  let pass = 0;
14
18
  let fail = 0;
@@ -25,8 +29,8 @@ const row = (o = {}) => JSON.stringify({
25
29
 
26
30
  const G = (root, args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' });
27
31
  const commitAll = (root, msg) => { G(root, ['add', '-A']); G(root, ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', msg]); };
28
- const run = (root, args = []) => {
29
- const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
32
+ const run = (root, args = [], env = {}) => {
33
+ const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8', env: { ...process.env, ...env } });
30
34
  return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
31
35
  };
32
36
 
@@ -46,6 +50,19 @@ const mkrepo = (firstRow = row()) => {
46
50
  const ledPath = (d) => path.join(d, '.agents', 'confirmations.jsonl');
47
51
  const append = (d, line) => fs.appendFileSync(ledPath(d), line + '\n');
48
52
 
53
+ // ---- 分片台账夹具(追加合法 / 改写硬拦 / 归并后删片合法 / 未归并存片硬拦)----
54
+ // 夹具注意:分片行的 doc 必须在树上有对应文件,否则行级校验「doc 不存在」会盖过被测判据。
55
+ const shardDirOf = (d) => path.join(d, '.agents', 'confirmations.d');
56
+ const mkDoc = (d, name) => fs.writeFileSync(path.join(d, 'workflow', 'intents', `${name}.md`), `---\n状态: approved\n级别: L1\n日期: 2026-10-02\n模块: pipeline\n---\n# ${name}\n`);
57
+ const shardWrite = (d, name, rows) => {
58
+ fs.mkdirSync(shardDirOf(d), { recursive: true });
59
+ fs.writeFileSync(path.join(shardDirOf(d), `${name}.jsonl`), rows.join('\n') + '\n');
60
+ };
61
+ // 特许修剪夹具
62
+ const sha256Of = (s) => spawnSync(process.execPath, ['-e', `console.log(require('crypto').createHash('sha256').update(process.argv[1],'utf8').digest('hex'))`, s], { encoding: 'utf8' }).stdout.trim();
63
+ const manifestPath = (d) => path.join(d, ...PRUNE_REL.split('/'));
64
+ const writeManifest = (d, obj) => { fs.mkdirSync(path.dirname(manifestPath(d)), { recursive: true }); fs.writeFileSync(manifestPath(d), JSON.stringify(obj, null, 2) + '\n'); };
65
+
49
66
  // S1:合法追加 ×2 → 历史全扫过
50
67
  {
51
68
  const d = mkrepo();
@@ -177,5 +194,418 @@ const append = (d, line) => fs.appendFileSync(ledPath(d), line + '\n');
177
194
  fs.rmSync(d, { recursive: true, force: true });
178
195
  }
179
196
 
197
+ // ---- S13-S17:--staged 分片触发面(2026-10-13 ledger-shard-default:分片为默认写入口径,触发面扩大)----
198
+ {
199
+ const d = mkrepo();
200
+ const shardDir = path.join(d, '.agents', 'confirmations.d');
201
+ fs.mkdirSync(shardDir, { recursive: true });
202
+ const shardP = path.join(shardDir, 'demo.jsonl');
203
+ fs.writeFileSync(shardP, row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done' }) + '\n');
204
+ G(d, ['add', '-A']);
205
+ let r = run(d, ['--staged']);
206
+ check('S13 --staged 新分片首入库(无 HEAD 基线)→ exit 0', r.code === 0, r.out);
207
+ commitAll(d, 'shard base');
208
+ r = run(d);
209
+ check('S13 分片入库后历史全扫 exit 0(分片历史干净)', r.code === 0, r.out);
210
+
211
+ // S14:分片纯追加 → --staged 放行
212
+ fs.appendFileSync(shardP, row({ ts: '2026-10-02T03:00:00.000Z', stage: 'done' }) + '\n');
213
+ G(d, ['add', '-A']);
214
+ r = run(d, ['--staged']);
215
+ check('S14 --staged 分片纯追加 → exit 0', r.code === 0, r.out);
216
+ commitAll(d, 'shard append');
217
+
218
+ // S15:分片重写 → --staged 拦(分片同受 append-only 前缀不变量约束)
219
+ const lines = fs.readFileSync(shardP, 'utf8').split('\n').filter(Boolean);
220
+ lines[0] = lines[0].replace('确认', '篡改后');
221
+ fs.writeFileSync(shardP, lines.join('\n') + '\n');
222
+ G(d, ['add', '-A']);
223
+ r = run(d, ['--staged']);
224
+ check('S15 --staged 分片重写 → exit 1(报文点名分片重写)', r.code === 1 && r.out.includes('分片重写'), r.out);
225
+ G(d, ['checkout', 'HEAD', '--', '.agents/confirmations.d/demo.jsonl']); // 还原到 HEAD 基线
226
+
227
+ // S16:compact 归并删分片(分片行全部并入暂存主台账)→ --staged 放行
228
+ fs.appendFileSync(ledPath(d), fs.readFileSync(shardP, 'utf8'));
229
+ fs.rmSync(shardP);
230
+ G(d, ['add', '-A']);
231
+ r = run(d, ['--staged']);
232
+ check('S16 --staged compact 归并删分片(行全并入暂存主台账)→ exit 0', r.code === 0, r.out);
233
+ commitAll(d, 'compact merge');
234
+
235
+ // S17:未并入即删分片(丢行)→ --staged 拦
236
+ fs.writeFileSync(shardP, row({ ts: '2026-10-02T04:00:00.000Z', stage: 'done' }) + '\n');
237
+ G(d, ['add', '-A']);
238
+ commitAll(d, 'shard again');
239
+ fs.rmSync(shardP);
240
+ G(d, ['add', '-A']);
241
+ r = run(d, ['--staged']);
242
+ check('S17 --staged 未并入即删分片 → exit 1(报文点名分片删除丢行)', r.code === 1 && r.out.includes('分片删除丢行'), r.out);
243
+ fs.rmSync(d, { recursive: true, force: true });
244
+ }
245
+
246
+ // ---- 分片台账不变量(2026-10-12 ledger-shard-write 实战套件移植):----
247
+ // H1:分片合法追加 ×2 → 历史全扫过(跨端确认不同 doc 写不同分片,互不干扰)
248
+ // H2:分片删历史行 → 前缀破坏硬拦(--staged 与历史全扫双态)
249
+ // H3:分片归并后删除(compact-ledgers 形态:全部行已逐字进主台账)→ 合法且 loud 记录
250
+ // H4:分片删除但行未进主台账(抹账)→ 硬拦
251
+ // H5:主台账删除仍恒拦(分片的合法删除形态不外溢到主台账)
252
+ // H6:分片行级违例(ts 乱序)在分片自身被检出,不因主台账无恙而漏
253
+ // (判据适配:kit 行级 ts 口径为「同 doc 内非降序」,报文措辞「追加时序倒挂」——见脚本头注①)
254
+ {
255
+ const d = mkrepo();
256
+ mkDoc(d, 'a');
257
+ shardWrite(d, 'doc-a', [row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/a.md' })]);
258
+ commitAll(d, 'shard doc-a v1');
259
+ fs.appendFileSync(path.join(shardDirOf(d), 'doc-a.jsonl'), row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done', doc: 'workflow/intents/a.md' }) + '\n');
260
+ commitAll(d, 'shard doc-a v2');
261
+ const r = run(d);
262
+ check('H1 分片合法追加两次 → 历史全扫 exit 0', r.code === 0, r.out);
263
+ fs.rmSync(d, { recursive: true, force: true });
264
+ }
265
+ {
266
+ const d = mkrepo();
267
+ mkDoc(d, 'b');
268
+ shardWrite(d, 'doc-b', [
269
+ row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/b.md' }),
270
+ row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done', doc: 'workflow/intents/b.md' }),
271
+ ]);
272
+ commitAll(d, 'shard doc-b v1');
273
+ shardWrite(d, 'doc-b', [row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/b.md' })]); // 删第二行
274
+ G(d, ['add', '-A']);
275
+ const rs = run(d, ['--staged']);
276
+ check('H2a 分片删历史行 → --staged 前缀破坏 exit 1', rs.code === 1 && /分片重写|台账重写/.test(rs.out) && /doc-b\.jsonl/.test(rs.out), rs.out);
277
+ commitAll(d, 'shard doc-b tampered');
278
+ const rh = run(d);
279
+ check('H2b 分片删历史行 → 历史全扫 exit 1 且点名分片', rh.code === 1 && /confirmations\.d\/doc-b\.jsonl/.test(rh.out), rh.out);
280
+ fs.rmSync(d, { recursive: true, force: true });
281
+ }
282
+ {
283
+ const d = mkrepo();
284
+ mkDoc(d, 'c');
285
+ const s1 = row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/c.md' });
286
+ const s2 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done', doc: 'workflow/intents/c.md' });
287
+ shardWrite(d, 'doc-c', [s1, s2]);
288
+ commitAll(d, 'shard doc-c');
289
+ // 归并:分片两行原样追加进主台账,再删分片(compact-ledgers.mjs 的真实动作序列)
290
+ append(d, s1); append(d, s2);
291
+ fs.rmSync(path.join(shardDirOf(d), 'doc-c.jsonl'));
292
+ commitAll(d, 'compact doc-c into main');
293
+ const r = run(d);
294
+ check('H3 分片归并后删除 → exit 0 且 loud 记录「归并后删除」', r.code === 0 && /归并后删除/.test(r.out), r.out);
295
+ fs.rmSync(d, { recursive: true, force: true });
296
+ }
297
+ {
298
+ const d = mkrepo();
299
+ mkDoc(d, 'e');
300
+ const s1 = row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/e.md' });
301
+ shardWrite(d, 'doc-e', [s1]);
302
+ commitAll(d, 'shard doc-e');
303
+ fs.rmSync(path.join(shardDirOf(d), 'doc-e.jsonl')); // 直接删,不归并
304
+ commitAll(d, 'delete shard without merge');
305
+ const r = run(d);
306
+ check('H4 分片删除而行未归并主台账 → hard 拦「分片删除」', r.code === 1 && /分片删除/.test(r.out) && /不在同提交主台账内/.test(r.out), r.out);
307
+ fs.rmSync(d, { recursive: true, force: true });
308
+ }
309
+ {
310
+ const d = mkrepo();
311
+ fs.rmSync(ledPath(d));
312
+ commitAll(d, 'delete main ledger');
313
+ const r = run(d);
314
+ check('H5 主台账删除 → 恒 hard 拦「台账删除」', r.code === 1 && /台账删除/.test(r.out), r.out);
315
+ fs.rmSync(d, { recursive: true, force: true });
316
+ }
317
+ {
318
+ const d = mkrepo();
319
+ mkDoc(d, 'f');
320
+ shardWrite(d, 'doc-f', [
321
+ row({ ts: '2026-10-02T05:00:00.000Z', doc: 'workflow/intents/f.md' }),
322
+ row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/f.md' }), // 早于前行
323
+ ]);
324
+ commitAll(d, 'shard doc-f out-of-order');
325
+ const r = run(d);
326
+ check('H6 分片内 ts 倒挂 → 行级拦且点名分片', r.code === 1 && /追加时序倒挂/.test(r.out) && /doc-f\.jsonl/.test(r.out), r.out);
327
+ fs.rmSync(d, { recursive: true, force: true });
328
+ }
329
+
330
+ // ---- 特许修剪(2026-10-09 user-directed-prune):manifest 逐行对账 ----
331
+ // P1:--staged 特许修剪——删中段行 + manifest 恰对账 → 放行
332
+ {
333
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/gone.md' });
334
+ const r3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done' });
335
+ const d = mkrepo();
336
+ append(d, r2); append(d, r3); commitAll(d, '3 rows');
337
+ fs.writeFileSync(ledPath(d), [row(), r3].join('\n') + '\n'); // 删 r2
338
+ writeManifest(d, { ts: '2026-10-09T00:00:00.000Z', authority: 'test', reason: 'test prune', removes: [sha256Of(r2)], docs: ['gone'] });
339
+ G(d, ['add', '-A']);
340
+ const r = run(d, ['--staged']);
341
+ check('P1 --staged 特许修剪 manifest 对账 → exit 0 且 loud 记录', r.code === 0 && /特许修剪通过/.test(r.out) && /1 行/.test(r.out), r.out);
342
+ commitAll(d, 'sanctioned prune');
343
+ const rh = run(d);
344
+ check('P1b 历史全扫认领同一修剪 → exit 0', rh.code === 0, rh.out);
345
+ fs.rmSync(d, { recursive: true, force: true });
346
+ }
347
+ // P2:--staged 删行但 manifest 哈希不对(对账失败)→ 拦
348
+ {
349
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/gone.md' });
350
+ const d = mkrepo();
351
+ append(d, r2); commitAll(d, '2 rows');
352
+ fs.writeFileSync(ledPath(d), row() + '\n');
353
+ writeManifest(d, { ts: '2026-10-09T00:00:00.000Z', authority: 'test', reason: 'x', removes: ['f'.repeat(64)], docs: ['gone'] });
354
+ G(d, ['add', '-A']);
355
+ const r = run(d, ['--staged']);
356
+ check('P2 manifest 哈希不命中 → exit 1 且报对账失败', r.code === 1 && r.out.includes('特许修剪对账失败'), r.out);
357
+ fs.rmSync(d, { recursive: true, force: true });
358
+ }
359
+ // P3:--staged 删行且无 manifest → 仍拦(append-only 红线对未授权场景不变)
360
+ {
361
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/gone.md' });
362
+ const d = mkrepo();
363
+ append(d, r2); commitAll(d, '2 rows');
364
+ fs.writeFileSync(ledPath(d), row() + '\n');
365
+ G(d, ['add', '-A']);
366
+ const r = run(d, ['--staged']);
367
+ check('P3 无 manifest 删行 → exit 1(S10 语义回归)', r.code === 1 && r.out.includes('台账重写'), r.out);
368
+ fs.rmSync(d, { recursive: true, force: true });
369
+ }
370
+ // P4:manifest 在场但删了 manifest 之外的行(夹带私删)→ 拦
371
+ {
372
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/gone.md' });
373
+ const r3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done', doc: 'workflow/intents/sneak.md' });
374
+ const d = mkrepo();
375
+ append(d, r2); append(d, r3); commitAll(d, '3 rows');
376
+ fs.writeFileSync(ledPath(d), [row(), r3].join('\n') + '\n'); // 删 r2;r3 保留
377
+ writeManifest(d, { ts: '2026-10-09T00:00:00.000Z', authority: 't', reason: 'x', removes: [sha256Of(r2), sha256Of(r3)], docs: ['gone', 'sneak'] }); // 多报一行
378
+ G(d, ['add', '-A']);
379
+ const r = run(d, ['--staged']);
380
+ check('P4 manifest 多报(哈希命中 1 ≠ 声明 2)→ exit 1', r.code === 1 && r.out.includes('特许修剪对账失败'), r.out);
381
+ fs.rmSync(d, { recursive: true, force: true });
382
+ }
383
+
384
+ // ---- ④ 归档感知(2026-10-13 archive-aware-doc):关单即归档移动文件后行仍指向真实文档 ----
385
+ // A1:行指向归档批次中的文档 → 放行;A2:行指向真消失文档(四目录与归档批次均无)→ 拦且报新口径
386
+ {
387
+ const d = mkrepo();
388
+ fs.mkdirSync(path.join(d, 'workflow', 'archive', '2026-10', 'plans'), { recursive: true });
389
+ fs.writeFileSync(path.join(d, 'workflow', 'archive', '2026-10', 'plans', 'archived.md'), '---\n状态: done\n---\n# A\n');
390
+ append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/plans/archived.md' }));
391
+ commitAll(d, 'append archived-doc row');
392
+ const r = run(d);
393
+ check('A1 doc 已归档(archive/2026-10/plans)→ 行级放行 exit 0', r.code === 0, r.out);
394
+ fs.rmSync(d, { recursive: true, force: true });
395
+ }
396
+ {
397
+ const d = mkrepo();
398
+ append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/plans/vanished.md' }));
399
+ commitAll(d, 'append vanished-doc row');
400
+ const r = run(d);
401
+ check('A2 doc 真消失(无归档批次命中)→ 拦且报「归档批次均未命中」', r.code === 1 && r.out.includes('归档批次均未命中'), r.out);
402
+ fs.rmSync(d, { recursive: true, force: true });
403
+ }
404
+
405
+ // ---- 合并并集语义(2026-10-13 merge-union-aware):多父提交按「并集无丢失」判 ----
406
+ // M1:两侧各追加一行,合并结果 = 三行之并 → union 认可、全扫过
407
+ // M2:合并结果丢掉一侧的行 → 拦且报「合并丢失」
408
+ const mkMergeRepo = () => {
409
+ const d = mkrepo();
410
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
411
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' });
412
+ const r3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'fixed' });
413
+ G(d, ['checkout', '-qb', 'br']);
414
+ append(d, r2); commitAll(d, 'br append r2');
415
+ G(d, ['checkout', '-q', BR]);
416
+ append(d, r3); commitAll(d, 'main append r3');
417
+ G(d, ['merge', '--no-commit', 'br']); // 冲突(双改同文件)——预期非零,手工写并集
418
+ return { d, r2, r3 };
419
+ };
420
+ {
421
+ const { d, r2, r3 } = mkMergeRepo();
422
+ fs.writeFileSync(ledPath(d), [row(), r2, r3].join('\n') + '\n'); // union 结果
423
+ commitAll(d, 'merge union');
424
+ const r = run(d);
425
+ check('M1 合并=父母行集之并 → union 认可 exit 0', r.code === 0 && /union 合并语义认可/.test(r.out), r.out);
426
+ fs.rmSync(d, { recursive: true, force: true });
427
+ }
428
+ {
429
+ const { d, r2 } = mkMergeRepo();
430
+ fs.writeFileSync(ledPath(d), [row(), r2].join('\n') + '\n'); // 丢了 main 侧 r3
431
+ commitAll(d, 'merge losing main-side line');
432
+ const r = run(d);
433
+ check('M2 合并丢行 → exit 1 且报「合并丢失」', r.code === 1 && r.out.includes('合并丢失'), r.out);
434
+ fs.rmSync(d, { recursive: true, force: true });
435
+ }
436
+
437
+ // ---- 合并审计加固(复核②实修回归):ours 丢行 / 合并携带修剪 / 凭空造行 ----
438
+ // M3:-X ours 合并静默丢 br 侧行(合并树与第一父同 blob,前缀通过)→ union 无条件对账必须拦
439
+ {
440
+ const d = mkrepo();
441
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
442
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' });
443
+ G(d, ['checkout', '-qb', 'br']);
444
+ append(d, r2); commitAll(d, 'br append r2');
445
+ G(d, ['checkout', '-q', BR]);
446
+ const rM3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'fixed' });
447
+ append(d, rM3); commitAll(d, 'main append r3');
448
+ G(d, ['merge', '-X', 'ours', '-q', 'br', '-m', 'ours merge drops br line']); // 树=第一父,r2 静默消失
449
+ const r = run(d);
450
+ check('M3 ours 合并静默丢行 → exit 1 且报「合并丢失」', r.code === 1 && r.out.includes('合并丢失'), r.out.slice(-300));
451
+ fs.rmSync(d, { recursive: true, force: true });
452
+ }
453
+ // M4:合并丢弃父母并集中的行 + manifest——(a) 诚实全量申报(对父母并集)→ 放行;(b) 只报第一父相对差集(p2 独有行无凭据消失)→ 拦
454
+ {
455
+ const d = mkrepo();
456
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
457
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' });
458
+ const r3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'fixed' });
459
+ G(d, ['checkout', '-qb', 'br']);
460
+ append(d, r2); commitAll(d, 'br r2');
461
+ G(d, ['checkout', '-q', BR]);
462
+ append(d, r3); commitAll(d, 'main r3');
463
+ G(d, ['merge', '--no-commit', 'br']);
464
+ fs.writeFileSync(ledPath(d), row() + '\n'); // r2、r3 全丢
465
+ writeManifest(d, { ts: '2026-10-13T00:00:00.000Z', authority: 't', reason: 'honest full prune', removes: [sha256Of(r2), sha256Of(r3)], docs: ['demo', 'demo'] });
466
+ commitAll(d, 'merge with honest full manifest');
467
+ const r = run(d);
468
+ check('M4a 合并丢双侧行+诚实全量 manifest → 集合对账放行 exit 0', r.code === 0 && /合并携带特许修剪/.test(r.out), r.out.slice(-300));
469
+ fs.rmSync(d, { recursive: true, force: true });
470
+ }
471
+ {
472
+ const d = mkrepo();
473
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
474
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' });
475
+ const r3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'fixed' });
476
+ G(d, ['checkout', '-qb', 'br']);
477
+ append(d, r2); commitAll(d, 'br r2');
478
+ G(d, ['checkout', '-q', BR]);
479
+ append(d, r3); commitAll(d, 'main r3');
480
+ G(d, ['merge', '--no-commit', 'br']);
481
+ fs.writeFileSync(ledPath(d), row() + '\n'); // r2、r3 全丢
482
+ writeManifest(d, { ts: '2026-10-13T00:00:00.000Z', authority: 't', reason: 'p1-relative partial', removes: [sha256Of(r3)], docs: ['demo'] }); // 只报 main 侧,br 的 r2 无凭据消失
483
+ commitAll(d, 'merge with partial manifest');
484
+ const r = run(d);
485
+ check('M4b 合并丢行+部分 manifest(p2 行无凭据)→ exit 1', r.code === 1 && /修剪集合对账/.test(r.out), r.out.slice(-300));
486
+ fs.rmSync(d, { recursive: true, force: true });
487
+ }
488
+ // M5:合并凭空多出父母都没有的行 → 拦
489
+ {
490
+ const d = mkrepo();
491
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
492
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' });
493
+ const r3 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'fixed' });
494
+ G(d, ['checkout', '-qb', 'br']);
495
+ append(d, r2); commitAll(d, 'br r2');
496
+ G(d, ['checkout', '-q', BR]);
497
+ append(d, r3); commitAll(d, 'main r3'); // 双侧分叉,保证是真合并而非 fast-forward
498
+ G(d, ['merge', '--no-commit', 'br']);
499
+ append(d, row({ ts: '2026-10-02T09:00:00.000Z', quote: '凭空造行' }));
500
+ commitAll(d, 'merge with fabricated line');
501
+ const r = run(d);
502
+ check('M5 合并凭空多出行 → exit 1 且报「凭空多出」', r.code === 1 && r.out.includes('凭空多出'), r.out.slice(-300));
503
+ fs.rmSync(d, { recursive: true, force: true });
504
+ }
505
+ // M6:合并收口删片(首父侧早已归并删片、分片仅第二父仍持有)→ 各父并集对账认可
506
+ // (合并感知删片①:单读首父只看一侧会把合法收口误判成「增删同提交」——首父盲区形态)
507
+ // 形态:A 首父侧建分片 → B 首父侧归并删片 → br(第二父,模拟他端未同步修剪)仍持分片并
508
+ // 独立追加主台账 → 真合并,分片按首父态在结果中缺席、主台账取父母并集。
509
+ {
510
+ const d = mkrepo();
511
+ mkDoc(d, 'm');
512
+ const s1 = row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/m.md' });
513
+ const r2 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done' });
514
+ const r9 = row({ ts: '2026-10-02T03:00:00.000Z', stage: 'fixed' });
515
+ shardWrite(d, 'doc-m', [s1]);
516
+ commitAll(d, 'A main create shard doc-m');
517
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
518
+ G(d, ['checkout', '-qb', 'br']); // br 自 A 分叉:仍持有分片(他端未做归并)
519
+ append(d, r2);
520
+ commitAll(d, 'C br append r2 (shard still present)');
521
+ G(d, ['checkout', '-q', BR]);
522
+ append(d, s1); // B 首父侧归并:行进主台账后删片(compact-ledgers 动作序列)
523
+ fs.rmSync(path.join(shardDirOf(d), 'doc-m.jsonl'));
524
+ commitAll(d, 'B main compact doc-m (append row then drop shard)');
525
+ append(d, r9);
526
+ commitAll(d, 'D main unrelated append');
527
+ G(d, ['merge', '--no-commit', 'br']); // 主台账双侧追加 → 冲突,手工写并集;分片呈删除态(delete/modify 自动取删除)
528
+ fs.writeFileSync(ledPath(d), [row(), s1, r2, r9].join('\n') + '\n');
529
+ commitAll(d, 'merge: ledger union + shard stays dropped');
530
+ const r = run(d);
531
+ check('M6 合并收口删片(分片仅第二父有)→ 各父并集对账认可 exit 0', r.code === 0 && /归并后删除/.test(r.out), r.out.slice(-400));
532
+ fs.rmSync(d, { recursive: true, force: true });
533
+ }
534
+ // M7:合并收口删片而行未归并主台账(借合并收口抹账)→ 硬拦
535
+ {
536
+ const d = mkrepo();
537
+ mkDoc(d, 'n');
538
+ const s1 = row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/n.md' });
539
+ const r2 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done' });
540
+ const r9 = row({ ts: '2026-10-02T03:00:00.000Z', stage: 'fixed' });
541
+ shardWrite(d, 'doc-n', [s1]);
542
+ commitAll(d, 'A main create shard doc-n');
543
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
544
+ G(d, ['checkout', '-qb', 'br']);
545
+ append(d, r2);
546
+ commitAll(d, 'C br append r2 (shard still present, rows never in main ledger)');
547
+ G(d, ['checkout', '-q', BR]);
548
+ append(d, r9); // 首父侧【不归并】直接独立推进
549
+ commitAll(d, 'D main unrelated append (no compaction)');
550
+ G(d, ['merge', '--no-commit', 'br']);
551
+ fs.writeFileSync(ledPath(d), [row(), r2, r9].join('\n') + '\n'); // 并集无 s1——分片行从未进主台账
552
+ fs.rmSync(path.join(shardDirOf(d), 'doc-n.jsonl')); // 合并解析把分片一并丢弃 → 借收口抹账
553
+ commitAll(d, 'merge: drop shard without compaction');
554
+ const r = run(d);
555
+ check('M7 合并收口删片未归并 → exit 1 且报「分片删除」', r.code === 1 && /分片删除/.test(r.out) && /不在同提交主台账内/.test(r.out), r.out.slice(-400));
556
+ fs.rmSync(d, { recursive: true, force: true });
557
+ }
558
+ // M8:合并删片缺行但终树已补登(merge-shard-restore-aware)→ 丢失追认 exit 0 且 loud
559
+ // 形态:br 侧分片多出 s2 未归并主台账,合并解析删片(s2 丢失)→ 后续提交把 s2 以补偿形态
560
+ // (ts 改现时刻 + note 引原 ts,doc/stage/fingerprint 沿原行)回append进主台账 → 语义身份比对追认。
561
+ {
562
+ const d = mkrepo();
563
+ mkDoc(d, 'p');
564
+ mkDoc(d, 'p2');
565
+ const s1 = row({ ts: '2026-10-02T01:00:00.000Z', doc: 'workflow/intents/p.md' });
566
+ const s2 = row({ ts: '2026-10-02T04:00:00.000Z', doc: 'workflow/intents/p2.md' });
567
+ const r2 = row({ ts: '2026-10-02T02:00:00.000Z', stage: 'done' });
568
+ const r9 = row({ ts: '2026-10-02T03:00:00.000Z', stage: 'fixed' });
569
+ shardWrite(d, 'doc-p', [s1]);
570
+ commitAll(d, 'A main create shard doc-p');
571
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
572
+ G(d, ['checkout', '-qb', 'br']);
573
+ append(d, r2);
574
+ fs.appendFileSync(path.join(shardDirOf(d), 'doc-p.jsonl'), s2 + '\n'); // br 侧分片多出 s2(未归并)
575
+ commitAll(d, 'C br append r2 + shard gains s2 (not compacted)');
576
+ G(d, ['checkout', '-q', BR]);
577
+ append(d, s1); // 首父侧归并 s1 后删片
578
+ fs.rmSync(path.join(shardDirOf(d), 'doc-p.jsonl'));
579
+ commitAll(d, 'B main compact s1 + drop shard');
580
+ append(d, r9);
581
+ commitAll(d, 'D main unrelated append');
582
+ G(d, ['merge', '--no-commit', 'br']);
583
+ fs.writeFileSync(ledPath(d), [row(), s1, r2, r9].join('\n') + '\n'); // 并集(无 s2——s2 在合并中丢失)
584
+ fs.rmSync(path.join(shardDirOf(d), 'doc-p.jsonl')); // 合并解析按首父态删片
585
+ commitAll(d, 'merge: s2 lost by shard drop');
586
+ const s2c = JSON.stringify({ ...JSON.parse(s2), ts: '2026-10-02T05:00:00.000Z', note: '补偿行:原行 ts 2026-10-02T04:00:00.000Z 合并丢失后补登' });
587
+ append(d, s2c); // 终树补登:s2 以**补偿形态**回append(doc/stage/fingerprint 沿原行)
588
+ commitAll(d, 'E restore lost s2 into main ledger (compensation form)');
589
+ const r = run(d);
590
+ check('M8 合并删片缺行终树已补登(补偿形态,语义身份比对)→ 丢失追认 exit 0', r.code === 0 && /补登/.test(r.out) && /丢失追认/.test(r.out), r.out.slice(-400));
591
+ fs.rmSync(d, { recursive: true, force: true });
592
+ }
593
+
594
+ // ---- 历史基线(2026-10-13 ledger-history-baseline):owner 声明豁免前基线时代 ----
595
+ // B1:前基线改写无凭据——无基线全史扫描拦;LEDGER_HISTORY_BASELINE 指到 HEAD 后豁免过
596
+ // (kit 缺省无基线 = 全量扫描,与源实现「缺省基线指向自身仓提交、外来仓退化为全扫」同语义)
597
+ {
598
+ const d = mkrepo();
599
+ append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
600
+ commitAll(d, 'append');
601
+ fs.writeFileSync(ledPath(d), row({ ts: '2026-10-02T02:00:00.000Z', quote: '改写' }) + '\n'); // 非 append 改写,无 manifest
602
+ commitAll(d, 'rewrite pre-baseline');
603
+ const head = G(d, ['rev-parse', 'HEAD']).stdout.trim();
604
+ const rNoBase = run(d); // kit 缺省无基线 → 全史扫描拦
605
+ const rBase = run(d, [], { LEDGER_HISTORY_BASELINE: head });
606
+ check('B1 历史基线:无基线改写拦 / 基线=HEAD 豁免过且 loud', rNoBase.code === 1 && rBase.code === 0 && /豁免/.test(rBase.out), `noBase=${rNoBase.out.slice(-200)} base=${rBase.out.slice(-200)}`);
607
+ fs.rmSync(d, { recursive: true, force: true });
608
+ }
609
+
180
610
  console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
181
- process.exit(fail ? 1 : 0);
611
+ process.exit(fail ? 1 : 0);
@@ -220,7 +220,7 @@ try {
220
220
  fs.mkdirSync(path.join(tree, '.agents', 'scripts'), { recursive: true });
221
221
  copyAgentsFiles(path.join(tree, '.agents'), ['workflow-enums.txt']);
222
222
  // 索引生成器 + 预算判定件的整闭包(含 workflow-enums.mjs 与日后新增依赖)
223
- copyScriptClosure(path.join(tree, '.agents', 'scripts'), ['gen-workflow-index.mjs', 'rule-budget.sh']);
223
+ copyScriptClosure(path.join(tree, '.agents', 'scripts'), ['gen-workflow-index.mjs', 'rule-budget.mjs']);
224
224
  fs.writeFileSync(path.join(tree, '.agents', 'rule-budgets.txt'), 'big.txt 10\n');
225
225
  fs.writeFileSync(path.join(tree, 'big.txt'), 'ok\n');
226
226
  for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
@@ -239,7 +239,8 @@ try {
239
239
  gitRun(tree, ['add', '--', '.']);
240
240
  gitRun(tree, ['commit', '-q', '-m', 'drift']);
241
241
  const drift = gitRun(tree, ['rev-parse', 'HEAD']);
242
- const drifted = loop(tree, ['--rev', drift]);
242
+ // --all 显式全量(2026-10-13 分道:本场景断言的是卫生面「索引漂移」告警)
243
+ const drifted = loop(tree, ['--rev', drift, '--all']);
243
244
  check('被推送提交索引损坏:--rev 报索引漂移', outOf(drifted).includes('索引漂移'), outOf(drifted));
244
245
  fs.writeFileSync(path.join(tree, 'big.txt'), 'x'.repeat(100));
245
246
  const restored = spawnSync(process.execPath, [path.join(tree, '.agents', 'scripts', 'gen-workflow-index.mjs')], { cwd: tree, encoding: 'utf8' });
@@ -250,8 +251,9 @@ try {
250
251
  fs.writeFileSync(path.join(tree, 'big.txt'), 'ok\n');
251
252
  gitRun(tree, ['add', '--', '.']);
252
253
  gitRun(tree, ['commit', '-q', '-m', 'slim']);
253
- const atFat = loop(tree, ['--rev', fat]);
254
- const atSlim = loop(tree, []);
254
+ // --all 同上:预算/索引漂移都是卫生面告警,须全量面才可见
255
+ const atFat = loop(tree, ['--rev', fat, '--all']);
256
+ const atSlim = loop(tree, ['--all']);
255
257
  check('预算只在被推送提交里超限:--rev 报 100B', outOf(atFat).includes('常驻面超限') && outOf(atFat).includes('100B'), outOf(atFat));
256
258
  check('工作区预算未超限:不带 --rev 不报常驻面超限,也不报索引漂移',
257
259
  !outOf(atSlim).includes('常驻面超限') && !outOf(atSlim).includes('索引漂移'), outOf(atSlim));
@@ -315,7 +317,8 @@ try {
315
317
  |------|----------|------------|------|------|
316
318
  | 2026-09-12 | x | y | 一次通过 | ${name} |
317
319
  `);
318
- const atOld = loop(repo, ['--rev', sha]);
320
+ // --all:委派台账对账是卫生面(检查 18),--rev 硬门面不含它
321
+ const atOld = loop(repo, ['--rev', sha, '--all']);
319
322
  check('委派行只在工作区:--rev 旧提交仍输出委派台账',
320
323
  outOf(atOld).includes('委派台账') && outOf(atOld).includes(`intents/${name}`),
321
324
  outOf(atOld));
@@ -42,6 +42,14 @@ const W = (root, rel, content) => { fs.mkdirSync(path.join(root, path.dirname(re
42
42
  // root 不可访问 → exit 1(不抛异常)
43
43
  const r3 = runCheckLoop({ root: path.join(root, '不存在目录') });
44
44
  check('run:root 不可访问 → exit 1(同款报错语义)', r3.exitCode === 1, `exit=${r3.exitCode}`);
45
+ // 分道 mode opt(2026-10-13 B6b-1):缺省=硬门(上方缺配对即拦);'hygiene' 只跑卫生段(硬违例不出);
46
+ // 'all' 两段都跑(等价分道前裸跑全量)。
47
+ const rHy = runCheckLoop({ root, mode: 'hygiene' });
48
+ check('run:mode=hygiene 只跑卫生段——L2 缺配对不出(exit 0 blockers 空)',
49
+ rHy.exitCode === 0 && rHy.blockers.length === 0, `exit=${rHy.exitCode} b=${rHy.blockers.length}`);
50
+ const rAll = runCheckLoop({ root, mode: 'all' });
51
+ check('run:mode=all 两段都跑——同样拦 L2 缺配对',
52
+ rAll.exitCode === 1 && rAll.blockers.some((b) => b.includes('配对断裂')), `exit=${rAll.exitCode}`);
45
53
  fs.rmSync(root, { recursive: true, force: true });
46
54
  }
47
55