agentic-flow-kit 1.7.0 → 1.7.16

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 (162) hide show
  1. package/README.md +81 -9
  2. package/modules/gates/README.md +1 -0
  3. package/modules/gates/test-protect/README.md +23 -0
  4. package/modules/gates/test-protect/check-test-protect.mjs +64 -0
  5. package/modules/gates/test-protect/local-pre-commit.line +1 -0
  6. package/modules/hosts/README.md +57 -0
  7. package/modules/hosts/claude/commands/wf-build.md +5 -24
  8. package/modules/hosts/claude/commands/wf-deploy.md +1 -0
  9. package/modules/hosts/claude/commands/wf-design.md +7 -6
  10. package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
  11. package/modules/hosts/claude/commands/wf-new-task.md +42 -21
  12. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  13. package/modules/hosts/claude/commands/wf-review.md +1 -1
  14. package/modules/hosts/claude/commands/wf-sync-hosts.md +8 -16
  15. package/modules/hosts/claude/commands/wf-test.md +5 -4
  16. package/modules/hosts/codex/commands/wf-build.md +5 -24
  17. package/modules/hosts/codex/commands/wf-deploy.md +1 -0
  18. package/modules/hosts/codex/commands/wf-design.md +7 -6
  19. package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
  20. package/modules/hosts/codex/commands/wf-new-task.md +42 -21
  21. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  22. package/modules/hosts/codex/commands/wf-review.md +1 -1
  23. package/modules/hosts/codex/commands/wf-sync-hosts.md +8 -16
  24. package/modules/hosts/codex/commands/wf-test.md +5 -4
  25. package/modules/hosts/cursor/commands/wf-build.md +5 -24
  26. package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
  27. package/modules/hosts/cursor/commands/wf-design.md +7 -6
  28. package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
  29. package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
  30. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  31. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  32. package/modules/hosts/cursor/commands/wf-sync-hosts.md +8 -16
  33. package/modules/hosts/cursor/commands/wf-test.md +5 -4
  34. package/modules/hosts/opencode/commands/wf-build.md +5 -24
  35. package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
  36. package/modules/hosts/opencode/commands/wf-design.md +7 -6
  37. package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
  38. package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
  39. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  40. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  41. package/modules/hosts/opencode/commands/wf-sync-hosts.md +8 -16
  42. package/modules/hosts/opencode/commands/wf-test.md +5 -4
  43. package/modules/hosts/trae/commands/wf-build.md +5 -24
  44. package/modules/hosts/trae/commands/wf-deploy.md +1 -0
  45. package/modules/hosts/trae/commands/wf-design.md +7 -6
  46. package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
  47. package/modules/hosts/trae/commands/wf-new-task.md +42 -21
  48. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  49. package/modules/hosts/trae/commands/wf-review.md +1 -1
  50. package/modules/hosts/trae/commands/wf-sync-hosts.md +8 -16
  51. package/modules/hosts/trae/commands/wf-test.md +5 -4
  52. package/package.json +2 -2
  53. package/src/add-gate.mjs +25 -1
  54. package/src/add-gate.test.mjs +103 -0
  55. package/src/cli.mjs +59 -5
  56. package/src/doctor.mjs +136 -12
  57. package/src/executor.mjs +245 -0
  58. package/src/executor.test.mjs +292 -0
  59. package/src/gates.test.mjs +17 -0
  60. package/src/init.mjs +14 -1
  61. package/src/init.test.mjs +19 -0
  62. package/src/instance-ref.test.mjs +62 -0
  63. package/src/jscpd-drift.test.mjs +144 -0
  64. package/src/profiles.mjs +3 -0
  65. package/src/render.mjs +6 -1
  66. package/src/rule-budget.test.mjs +103 -0
  67. package/src/sync-hosts.mjs +41 -45
  68. package/src/sync-hosts.test.mjs +67 -56
  69. package/src/trae-hooks.test.mjs +15 -3
  70. package/src/validate.mjs +72 -0
  71. package/src/validate.test.mjs +95 -0
  72. package/templates/.gitattributes +8 -0
  73. package/templates/AGENTS.md +2 -1
  74. package/templates/_agents/commands/build.md +5 -24
  75. package/templates/_agents/commands/deploy.md +1 -0
  76. package/templates/_agents/commands/design.md +7 -6
  77. package/templates/_agents/commands/gate-checklist.md +1 -3
  78. package/templates/_agents/commands/new-task.md +42 -21
  79. package/templates/_agents/commands/plan.md +1 -1
  80. package/templates/_agents/commands/review.md +1 -1
  81. package/templates/_agents/commands/sync-hosts.md +8 -16
  82. package/templates/_agents/commands/test.md +5 -4
  83. package/templates/_agents/control-surfaces.txt +53 -0
  84. package/templates/_agents/engine-lock.json +246 -0
  85. package/templates/_agents/lane-surfaces.txt +1 -1
  86. package/templates/_agents/level-hints.txt +30 -0
  87. package/templates/_agents/scripts/agg-delegations.cjs +51 -6
  88. package/templates/_agents/scripts/agg-delegations.test.mjs +84 -4
  89. package/templates/_agents/scripts/check-engine-integrity.mjs +217 -0
  90. package/templates/_agents/scripts/check-engine-integrity.test.mjs +215 -0
  91. package/templates/_agents/scripts/check-lane-surface.mjs +78 -24
  92. package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
  93. package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
  94. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
  95. package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
  96. package/templates/_agents/scripts/check-loop.mjs +255 -45
  97. package/templates/_agents/scripts/check-loop.test.mjs +330 -15
  98. package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
  99. package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
  100. package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
  101. package/templates/_agents/scripts/confirm-doc.mjs +54 -12
  102. package/templates/_agents/scripts/confirm-doc.test.mjs +59 -0
  103. package/templates/_agents/scripts/doctor.test.mjs +294 -1
  104. package/templates/_agents/scripts/draft-sediment.mjs +178 -0
  105. package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
  106. package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
  107. package/templates/_agents/scripts/fill-intent.mjs +2 -2
  108. package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
  109. package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
  110. package/templates/_agents/scripts/gate-checklist.mjs +6 -3
  111. package/templates/_agents/scripts/gate-checklist.test.mjs +2 -5
  112. package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
  113. package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
  114. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +10 -13
  115. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -30
  116. package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
  117. package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
  118. package/templates/_agents/scripts/kb-search.mjs +87 -12
  119. package/templates/_agents/scripts/kb-search.test.mjs +83 -0
  120. package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
  121. package/templates/_agents/scripts/ledger-loader.mjs +108 -0
  122. package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
  123. package/templates/_agents/scripts/ledger-prune.mjs +96 -0
  124. package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
  125. package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
  126. package/templates/_agents/scripts/level-criteria.mjs +232 -0
  127. package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
  128. package/templates/_agents/scripts/pipeline-run.mjs +5 -3
  129. package/templates/_agents/scripts/policy.mjs +47 -0
  130. package/templates/_agents/scripts/solidify-task.mjs +1 -1
  131. package/templates/_agents/scripts/stage-gates.mjs +52 -10
  132. package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
  133. package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
  134. package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
  135. package/templates/_agents/scripts/verify.mjs +1 -1
  136. package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
  137. package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
  138. package/templates/_agents/scripts/workflows-check.mjs +1 -1
  139. package/templates/_agents/scripts-test-exempt.txt +3 -1
  140. package/templates/_agents/skills/wiki/SKILL.md +5 -3
  141. package/templates/_agents/workflows/pipeline-closing.md +4 -5
  142. package/templates/_githooks/pre-commit +3 -34
  143. package/templates/_githooks/pre-push +3 -3
  144. package/templates/wiki/INDEX.md +44 -1
  145. package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
  146. package/templates/workflow/README.md +11 -0
  147. package/templates/workflow/delegations.md +5 -4
  148. package/templates/workflow/incidents/_TEMPLATE.md +2 -0
  149. package/templates/workflow/intents/_TEMPLATE.md +3 -0
  150. package/templates/workflow/plans/_TEMPLATE.md +4 -0
  151. package/templates/workflow/specs/_TEMPLATE.md +23 -0
  152. package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -91
  153. package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -91
  154. package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -91
  155. package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -91
  156. package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -92
  157. package/templates/_agents/commands/source-sync-check.md +0 -105
  158. package/templates/_agents/scripts/check-ledger.mjs +0 -109
  159. package/templates/_agents/scripts/check-ledger.test.mjs +0 -197
  160. package/templates/_agents/scripts/source-sync-check.mjs +0 -156
  161. package/templates/_agents/scripts/source-sync-check.test.mjs +0 -281
  162. package/templates/_agents/workflows/source-sync-repair.md +0 -30
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // compact-ledgers.test.mjs — 台账分片写入与 compact 归并 fixture 套件(2026-10-12 ledger-shard-write)
3
+ // 覆盖:分片写入定位 + 跨来源链衔接 / compact 默认 dry-run 不动盘 / 幂等 / 前缀不变式(既有行保序)/
4
+ // 读取失败中止 / 分片历史扫描与 compact 删除豁免 / 行未并入时仍拦丢行。
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ const { compactLedgers } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'compact-ledgers.mjs')).href);
13
+ const { appendLedger } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'confirm-doc.mjs')).href);
14
+ const { ledgerChainHash } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'policy.mjs')).href);
15
+ // 夹具闭包(2026-10-12 fixture-closure-rootfix):整闭包自动跟随,不再手写依赖枚举(漏拷 = 假绿)
16
+ const { copyScriptClosure } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'fixture-scripts.mjs')).href);
17
+
18
+ let pass = 0, fail = 0;
19
+ const check = (name, ok, detail = '') => {
20
+ if (ok) { pass++; console.log(` ok ${name}`); }
21
+ else { fail++; console.error(`FAIL ${name}${detail ? ' — ' + detail : ''}`); }
22
+ };
23
+
24
+ const DOC_A = 'workflow/intents/2026-10-01-a.md';
25
+ const DOC_B = 'workflow/intents/2026-10-01-b.md';
26
+ const baseEntry = (doc, stage = 'approved') => ({
27
+ ts: new Date().toISOString(), doc, stage, fingerprint: 'a'.repeat(64), source: 'tty',
28
+ });
29
+ // --shard 依赖 doc 链口径(policyVersion ≥ 8):分片与主台账是不同文件,只有 doc 链跨来源接续。
30
+ // 装 kit.json 的 policyVersion=8 来启用,与真实装户升级后的形态一致。
31
+ const enableDocChain = (root) => {
32
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
33
+ fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 8 }), 'utf8');
34
+ };
35
+
36
+ // ============ 场景 1:--shard 写入定位到分片,且跨来源链接得上 ============
37
+ {
38
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-write-'));
39
+ enableDocChain(root);
40
+ // 先用主台账写一行(模拟该 doc 的历史行在主台账)
41
+ appendLedger(root, baseEntry(DOC_A));
42
+ const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
43
+ check('S1 默认写主台账', fs.existsSync(mainPath));
44
+
45
+ // 再对同一 doc 用 --shard 写一行:前驱必须接主台账那行,而不是从 '' 重启
46
+ appendLedger(root, baseEntry(DOC_A, 'done'), { shard: true });
47
+ const shardPath = path.join(root, '.agents', 'confirmations.d', '2026-10-01-a.jsonl');
48
+ check('S1 --shard 写入分片文件(按 doc 基名命名)', fs.existsSync(shardPath));
49
+
50
+ const mainRows = fs.readFileSync(mainPath, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
51
+ const shardRows = fs.readFileSync(shardPath, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
52
+ check('S1 分片行的 prevHash 接主台账同 doc 行(跨来源衔接,链不断)',
53
+ shardRows[0].prevHash === mainRows[0].hash, `${shardRows[0].prevHash} vs ${mainRows[0].hash}`);
54
+ // 行内重算仍一致(doc 链前提)
55
+ const { hash, prevHash, ...rest } = shardRows[0];
56
+ check('S1 分片行行内哈希可重算', ledgerChainHash(rest, prevHash) === hash);
57
+ }
58
+
59
+ // ============ 场景 2:compact 默认 dry-run 不动盘 ============
60
+ {
61
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-dry-'));
62
+ appendLedger(root, baseEntry(DOC_A));
63
+ appendLedger(root, baseEntry(DOC_B), { shard: true });
64
+ const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
65
+ const before = fs.readFileSync(mainPath, 'utf8');
66
+ const shardPath = path.join(root, '.agents', 'confirmations.d', '2026-10-01-b.jsonl');
67
+
68
+ const r = compactLedgers(root); // 默认 execute:false
69
+ check('S2 dry-run 标记 skipped 且给出并入行数', r.ok && r.skipped && r.reason === 'dry-run' && r.added === 1, JSON.stringify(r));
70
+ check('S2 dry-run 主台账字节不变', fs.readFileSync(mainPath, 'utf8') === before);
71
+ check('S2 dry-run 分片仍在', fs.existsSync(shardPath));
72
+ }
73
+
74
+ // ============ 场景 3:--execute 归并 + 前缀不变式 ============
75
+ {
76
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-exec-'));
77
+ appendLedger(root, baseEntry(DOC_A));
78
+ const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
79
+ const before = fs.readFileSync(mainPath, 'utf8');
80
+ appendLedger(root, baseEntry(DOC_B), { shard: true });
81
+
82
+ const r = compactLedgers(root, { execute: true });
83
+ const after = fs.readFileSync(mainPath, 'utf8');
84
+ check('S3 execute 归并成功并计入 1 行', r.ok && !r.skipped && r.added === 1, JSON.stringify(r));
85
+ check('S3 既有行严格保序在前(append-only 前缀不变式)', after.startsWith(before), JSON.stringify(after.slice(0, 40)));
86
+ check('S3 分片已清理', !fs.existsSync(path.join(root, '.agents', 'confirmations.d', '2026-10-01-b.jsonl')));
87
+ }
88
+
89
+ // ============ 场景 4:幂等(重跑不重复追加)============
90
+ {
91
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-idem-'));
92
+ enableDocChain(root);
93
+ appendLedger(root, baseEntry(DOC_A));
94
+ appendLedger(root, baseEntry(DOC_B), { shard: true });
95
+ compactLedgers(root, { execute: true });
96
+ const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
97
+ const after1 = fs.readFileSync(mainPath, 'utf8');
98
+ const mergedLine = after1.trim().split('\n').pop(); // 归并时实际追加的那一行(含 prevHash/hash)
99
+
100
+ // 把**同一行原文**再放进分片重跑(模拟重复归并同批内容)——行文本逐字相同才会被去重
101
+ fs.mkdirSync(path.join(root, '.agents', 'confirmations.d'), { recursive: true });
102
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.d', '2026-10-01-b.jsonl'), mergedLine + '\n', 'utf8');
103
+ const r = compactLedgers(root, { execute: true });
104
+ const after2 = fs.readFileSync(mainPath, 'utf8');
105
+ check('S4 重复归并不重复追加(并集按整行去重)', r.ok && r.added === 0 && after1 === after2, `added=${r.added}`);
106
+ }
107
+
108
+ // ============ 场景 5:无分片 / 空分片 → 跳过且不报错 ============
109
+ {
110
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-none-'));
111
+ const r = compactLedgers(root);
112
+ check('S5 无分片目录 → 跳过且 ok', r.ok && r.skipped);
113
+ }
114
+
115
+ // ============ 场景 6:分片历史扫描 + compact 删除豁免(真 git)============
116
+ {
117
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-hist-'));
118
+ const git = (...args) => spawnSync('git', args, { cwd: d, encoding: 'utf8' });
119
+ git('init', '-q', '.'); git('config', 'user.email', 't@t.io'); git('config', 'user.name', 't');
120
+ git('config', 'core.autocrlf', 'false');
121
+ fs.mkdirSync(path.join(d, '.agents', 'scripts'), { recursive: true });
122
+ copyScriptClosure(path.join(d, '.agents', 'scripts'), ['check-ledger-invariant.mjs']);
123
+ const mkDoc = (rel) => {
124
+ const p = path.join(d, ...rel.split('/'));
125
+ fs.mkdirSync(path.dirname(p), { recursive: true });
126
+ fs.writeFileSync(p, '---\n状态: approved\n---\n# doc\n', 'utf8');
127
+ };
128
+ const rowObj = (doc) => JSON.stringify({ ts: '2026-10-01T00:00:00Z', doc, stage: 'approved', fingerprint: 'a'.repeat(64), source: 'tty' });
129
+ const runGate = () => spawnSync(process.execPath, ['.agents/scripts/check-ledger-invariant.mjs'], { cwd: d, encoding: 'utf8' });
130
+
131
+ mkDoc(DOC_A); mkDoc(DOC_B);
132
+ // 基线:主台账一行 + 分片一行(分片持有 B 的行)
133
+ fs.mkdirSync(path.join(d, '.agents', 'confirmations.d'), { recursive: true });
134
+ fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowObj(DOC_A) + '\n', 'utf8');
135
+ fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-b.jsonl'), rowObj(DOC_B) + '\n', 'utf8');
136
+ git('add', '-A'); git('commit', '-qm', 'base with shard');
137
+ let r = runGate();
138
+ check('S6 分片入库且未删 → 历史扫描放行', r.status === 0, `exit=${r.status} ${r.stderr}`);
139
+
140
+ // compact 归并后删分片:删除豁免应认领
141
+ fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowObj(DOC_A) + '\n' + rowObj(DOC_B) + '\n', 'utf8');
142
+ fs.rmSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-b.jsonl'));
143
+ git('add', '-A'); git('commit', '-qm', 'compact merged');
144
+ r = runGate();
145
+ check('S6 compact 归并删分片 → 删除豁免认领,exit 0', r.status === 0, `exit=${r.status} ${r.stderr}`);
146
+ check('S6 豁免记录点名 compact 归并形态', /compact 归并形态/.test(r.stderr), r.stderr.slice(0, 200));
147
+
148
+ // 未归并就删分片(丢行)→ 仍 hard 拦。用**从未进过主台账**的独立 doc C,
149
+ // 否则它的行已在主台账里(早先 compact 并过),删除豁免会正确地放行——那正是豁免该有的行为。
150
+ const DOC_C = 'workflow/intents/2026-10-01-c.md';
151
+ mkDoc(DOC_C);
152
+ fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-c.jsonl'), rowObj(DOC_C) + '\n', 'utf8');
153
+ git('add', '-A'); git('commit', '-qm', 'add shard c');
154
+ fs.rmSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-c.jsonl'));
155
+ git('add', '-A'); git('commit', '-qm', 'drop shard c without merge');
156
+ r = runGate();
157
+ check('S6 未归并即删分片(丢行)→ exit 1 hard 拦', r.status === 1, `exit=${r.status}`);
158
+ check('S6 报文点名「分片删除丢行」', /分片删除丢行/.test(r.stderr), r.stderr.slice(0, 300));
159
+ }
160
+
161
+ console.log(`\n[compact-ledgers.test] PASS ${pass} / FAIL ${fail}`);
162
+ process.exit(fail ? 1 : 0);
@@ -25,6 +25,8 @@
25
25
  // node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<用户对话原话>"(委托代录)
26
26
  // node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<原话>" --batch(协作道批量:L0/L1 多份一次代录)
27
27
  // node .agents/scripts/confirm-doc.mjs <doc...> --to superseded|cancelled(放弃态:取代/取消,留指纹与台账)
28
+ // node .agents/scripts/confirm-doc.mjs <doc...> --shard(分片写入:多 worktree 并行时各写各的
29
+ // .agents/confirmations.d/<doc>.jsonl,避免同文件并发覆盖;里程碑跑 compact-ledgers.mjs 归并)
28
30
  // 多文档一次传入:TTY 模式天然逐份过目键入;--delegated 默认一次仅一份(2026-09-27
29
31
  // confirm-gate-one-per-call:多份并录曾系统性塌掉 build.md「逐件确认」三道门),L0/L1 协作道可
30
32
  // `--batch` 多份一次代录(2026-09-30 hybrid-governance-risk-lanes):逐份读「级别」,L2/L3 逐份拒绝
@@ -39,8 +41,9 @@ import path from 'node:path';
39
41
  import readline from 'node:readline';
40
42
  import { createHash, randomBytes } from 'node:crypto';
41
43
  import { spawnSync } from 'node:child_process';
42
- import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
43
- import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeDigest } from './policy.mjs';
44
+ import { readLedgerEntries } from './ledger-loader.mjs';
45
+ import { confirmGateFor, clarifyGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
46
+ import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, ledgerDocChainActive, ledgerDocChainPrev, scopeDigest } from './policy.mjs';
44
47
 
45
48
  // hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
46
49
  // policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
@@ -148,22 +151,38 @@ export function applyTransition(text, targetStage, fingerprint16) {
148
151
  }
149
152
 
150
153
  // appendLedger(root, entry):追加一行 JSON(目录不存在自动建;只 append 不重写)
151
- // 哈希链(2026-10-09 engine-quality-round2 D):每行追加 prevHash(末行 hash,末行无 hash/空文件 → '')
152
- // 与 hash(ledgerChainHash 单源计算,check-loop 15 同函数验链)——「就地改/删中间行」必留断链痕迹;
153
- // 历史行零回填(无 hash 行后链重启),整文件重写不可机器防(边界声明见 policy.ledgerChainHash)。
154
- export function appendLedger(root, entry) {
155
- const p = path.join(root, '.agents', 'confirmations.jsonl');
154
+ // 哈希链(2026-10-09 engine-quality-round2 D):每行追加 prevHash 与 hash(ledgerChainHash 单源计算,
155
+ // check-loop 15 同函数验链)——「就地改/删中间行」必留断链痕迹;历史行零回填(无 hash 行后链重启),
156
+ // 整文件重写不可机器防(边界声明见 policy.ledgerChainHash)。
157
+ // 链域(2026-10-12 ledger-doc-chain):policy.ledgerDocChainSince 存在且合法 → prevHash 取**同行 doc 的
158
+ // 最后一个带 hash 行**(ledgerDocChainPrev),不再取物理末行。理由见 POLICIES v8 注释:物理链隐含
159
+ // 「单写者顺序追加」前提,而并发 append / git 并集合并 / 分片归并三种**正常协作**都会打断它。
160
+ // 缺键(v1-v7)→ 走旧物理链口径,存量零变化。
161
+ // shard(2026-10-12 ledger-shard-write):多 worktree 并行时两个进程同时 append 同一台账文件会互相覆盖
162
+ // (各自读到同一末行 → 同 prevHash → 后写者覆盖前写者)。--shard 把本行写入分片
163
+ // `.agents/confirmations.d/<doc基名>.jsonl`,各 worktree 各写各的、互不冲突;里程碑跑 compact-ledgers.mjs
164
+ // 归并回主台账。**默认仍写主台账**——分片是有意的并发开关,不设则零影响。
165
+ export function appendLedger(root, entry, { shard = false } = {}) {
166
+ const p = shard && entry.doc
167
+ ? path.join(root, '.agents', 'confirmations.d', `${path.basename(entry.doc, '.md')}.jsonl`)
168
+ : path.join(root, '.agents', 'confirmations.jsonl');
156
169
  fs.mkdirSync(path.dirname(p), { recursive: true });
157
170
  let prevHash = '';
158
171
  try {
159
- if (fs.existsSync(p)) {
172
+ if (ledgerDocChainActive(loadKitPolicy(root))) {
173
+ // doc 链取前驱走**台账视图**(主台账 + 分片)而非单个文件:--shard 写入时,该 doc 的历史行
174
+ // 很可能在主台账里,只读分片文件会从 '' 重启 → 链断裂。视图取法天然跨来源衔接。
175
+ const rows = readLedgerEntries(root).map((e) => e.row);
176
+ prevHash = ledgerDocChainPrev(rows, entry.doc);
177
+ } else if (fs.existsSync(p)) {
178
+ // 旧物理链口径:取**本次写入目标文件**的末行 hash(v1-v7 存量行为,逐字不变)
160
179
  const lines = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter((l) => l.trim());
161
180
  if (lines.length) {
162
181
  const last = JSON.parse(lines[lines.length - 1]);
163
182
  prevHash = last && typeof last.hash === 'string' ? last.hash : '';
164
183
  }
165
184
  }
166
- } catch { /* 末行不可解析 → prevHash ''(链从本行重启;断裂由 check-loop 15 出账) */ }
185
+ } catch { /* 不可解析 → prevHash ''(链从本行重启;断裂由 check-loop 15 出账) */ }
167
186
  const { hash: _h, prevHash: _p, ...rest } = entry;
168
187
  const row = { ...rest, prevHash, hash: ledgerChainHash(rest, prevHash) };
169
188
  fs.appendFileSync(p, JSON.stringify(row) + '\n');
@@ -190,6 +209,7 @@ if (isMain) {
190
209
  let toTarget = null; // null = 默认前向跳转;superseded|cancelled = 放弃态显式目标
191
210
  let autoMode = false; // AI 自治模式(信任等级授权,2026-09-30 ai-autonomy-trust)
192
211
  let batchMode = false; // 协作道批量代录(--batch,2026-09-30 hybrid-governance-risk-lanes:仅 --delegated + L0/L1)
212
+ let shardWrite = false; // 分片写入(--shard,2026-10-12 ledger-shard-write):多 worktree 并行时各写各的分片,避免互相覆盖
193
213
  const docs = [];
194
214
  for (let i = 0; i < argv.length; i++) {
195
215
  if (argv[i] === '--root') root = path.resolve(argv[++i]);
@@ -197,12 +217,21 @@ if (isMain) {
197
217
  else if (argv[i] === '--to') toTarget = argv[++i] ?? '';
198
218
  else if (argv[i] === '--auto') autoMode = true;
199
219
  else if (argv[i] === '--batch') batchMode = true;
220
+ else if (argv[i] === '--shard') shardWrite = true;
200
221
  else docs.push(argv[i].replace(/\\/g, '/'));
201
222
  }
202
223
  if (toTarget !== null && !ABANDON_TARGETS.includes(toTarget)) {
203
224
  console.error(`用法:--to 仅接受 superseded|cancelled(现「${toTarget}」)——cancelled 自 draft/approved/open/fixed;superseded 自 approved/done/fixed/closed(未确认态只能 cancelled;终态间仅允许 done/closed→superseded)`);
204
225
  process.exit(1);
205
226
  }
227
+ // --shard 与 doc 链口径绑定(2026-10-12 ledger-shard-write):分片与主台账分属不同文件,
228
+ // 只有 doc 链能跨来源接续;旧物理链口径下分片行的 prevHash 会与主台账末行失配 → 提交即断链。
229
+ // 故缺锚时**参数期直接拒**(友好报错),而不是让用户写完台账再被门禁拦。
230
+ if (shardWrite && !ledgerDocChainActive(loadKitPolicy(root))) {
231
+ console.error('❌ --shard 需要台账 doc 链口径(policy.ledgerDocChainSince),当前 policyVersion 无此键。\n 原因:分片与主台账是不同文件,只有 doc 链能跨来源接续;旧物理链口径下分片行会与主台账断链。\n 修法:在 .agents/kit.json 设 policyVersion: 8(或更高的支持版本)后重试。');
232
+ process.exit(1);
233
+ }
234
+
206
235
  const delegated = delegatedQuote !== null;
207
236
  if (delegated && autoMode) {
208
237
  console.error('❌ --auto 与 --delegated 互斥:自治模式是用户预先授权的机器放行,委托代录是单次对话确认');
@@ -358,6 +387,19 @@ if (isMain) {
358
387
  refused++;
359
388
  continue;
360
389
  }
390
+ // 待澄清项门(2026-10-12 needs-clarification):起草期挂「待澄清」标记**不阻断**(零阻塞),
391
+ // 但 draft→approved 确认时若仍挂着未澄清项则拒绝——确认即「认可这份文档」,文档里还写着
392
+ // 「这里我不知道」,确认就没有意义。与上面的前置门分开出账:拒绝原因必须指向真正的原因
393
+ // (合并会让作者去查「前置」,而前置其实是好的)。
394
+ // ⚠️ 文案锚点硬约束:本段禁止并入上方 gate 分支的输出契约(既有 confirm-doc.test.mjs 断言
395
+ // 锚定「未过逐阶段前置门(stage-gates)」子串),故单列独立分支。
396
+ const cl = clarifyGateFor(abs);
397
+ if (!cl.ok) {
398
+ console.error(`❌ ${doc} 含未澄清项(needs-clarification):${cl.reason}`);
399
+ console.error(` ${cl.hint}(本份未落账、未写盘)`);
400
+ refused++;
401
+ continue;
402
+ }
361
403
  }
362
404
  // done 前置门(2026-09-30 confirm-gate-approved-history):→done 须 git 历史已留 approved 态
363
405
  // (先 done 后提交 / 两跳同批提交 → 拒绝;防「确认态缺失」缺陷复发;判据单源见 stage-gates.doneGateFor)。
@@ -407,7 +449,7 @@ if (isMain) {
407
449
  }
408
450
  fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
409
451
  appendLedger(root, {
410
- ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
452
+ shard: shardWrite, ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
411
453
  source: `ai-auto-trust-L${trustConfig.level}`, quote: `AI 自治放行 (Trust Level ${trustConfig.level})`,
412
454
  batch, seq, of: docs.length, scope,
413
455
  });
@@ -428,7 +470,7 @@ if (isMain) {
428
470
  }
429
471
  fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
430
472
  appendLedger(root, {
431
- ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
473
+ shard: shardWrite, ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
432
474
  source: 'chat-delegated', quote: String(delegatedQuote),
433
475
  // 调用事实(batch-ledger-audit):seq=本次调用内件序(从 1),of=本次调用总份数
434
476
  batch, seq, of: docs.length,
@@ -458,7 +500,7 @@ if (isMain) {
458
500
  }
459
501
  fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
460
502
  appendLedger(root, {
461
- ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
503
+ shard: shardWrite, ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
462
504
  source: 'tty',
463
505
  batch, seq, of: docs.length, // 调用事实(形态无关:TTY 多文档天然逐件过目,of>1 不构成违规)
464
506
  });
@@ -386,6 +386,65 @@ const readLedgerOf = (root) => {
386
386
  fs.rmSync(root, { recursive: true, force: true });
387
387
  }
388
388
 
389
+ // ---- S19b【待澄清门】spec 挂未澄清项 → 拒绝落账(2026-10-12 needs-clarification)----
390
+ // 夹具沿 S23 的正路入口(台账行 + approved),确保**前置门已过**——否则拒绝原因会指向前置而非待澄清,
391
+ // 用例就测不出新门。断言拒绝文案点名「needs-clarification」且**不含**前置门文案(两门分开出账)。
392
+ {
393
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl1-'));
394
+ for (const sub of ['intents', 'specs']) fs.mkdirSync(path.join(root, 'workflow', sub), { recursive: true });
395
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
396
+ const fp = 'c'.repeat(64);
397
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-12-n.md'), `---\n状态: approved\n级别: L2\n日期: 2026-10-12\n确认指纹: ${fp.slice(0, 16)}\n---\n# I\n`);
398
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-10-12T02:00:00.000Z', doc: 'workflow/intents/2026-10-12-n.md', stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 'n1', seq: 1, of: 1 }) + '\n');
399
+ const specP = path.join(root, 'workflow', 'specs', '2026-10-12-n.md');
400
+ fs.writeFileSync(specP, '---\n状态: draft\n级别: L2\n---\n# S\n\n## 功能行为\n- 待澄清:错误率的判据是 5xx 占比还是全量?\n');
401
+ const before = fs.readFileSync(specP, 'utf8');
402
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-10-12-n.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
403
+ const after = fs.readFileSync(specP, 'utf8');
404
+ const led = readLedgerOf(root);
405
+ check('S19b 待澄清门:挂未澄清项 → exit 2 + 点名行号与内容 + 未落盘 + 零新增台账',
406
+ r.status === 2 && /needs-clarification/.test(r.stderr) && /第8行/.test(r.stderr) && /5xx 占比还是全量/.test(r.stderr)
407
+ && after === before && led.length === 1,
408
+ JSON.stringify({ status: r.status, stderr: r.stderr, ledger: led.length }));
409
+ check('S19b 待澄清门:拒绝原因不误指向前置门(两门分开出账,否则作者查错地方)',
410
+ !/逐阶段前置门/.test(r.stderr), r.stderr);
411
+ fs.rmSync(root, { recursive: true, force: true });
412
+ }
413
+
414
+ // ---- S19c【待澄清门】解决后(行首改「已澄清」)→ 放行;纯删行亦可 ----
415
+ {
416
+ const mkClarified = (body) => {
417
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl2-'));
418
+ for (const sub of ['intents', 'specs']) fs.mkdirSync(path.join(root, 'workflow', sub), { recursive: true });
419
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
420
+ const fp = 'd'.repeat(64);
421
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-12-o.md'), `---\n状态: approved\n级别: L2\n日期: 2026-10-12\n确认指纹: ${fp.slice(0, 16)}\n---\n# I\n`);
422
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-10-12T02:00:00.000Z', doc: 'workflow/intents/2026-10-12-o.md', stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 'o1', seq: 1, of: 1 }) + '\n');
423
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-10-12-o.md'), body);
424
+ return root;
425
+ };
426
+ for (const [name, body] of [
427
+ ['改前缀为「已澄清」(保留行、留痕曾不清楚)', '---\n状态: draft\n级别: L2\n---\n# S\n\n- 已澄清:错误率判据 = 5xx 占比(与用户 10-12 对齐)\n'],
428
+ ['直接删行', '---\n状态: draft\n级别: L2\n---\n# S\n\n## 功能行为\n- 错误率判据 = 5xx 占比\n'],
429
+ ]) {
430
+ const root = mkClarified(body);
431
+ const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-10-12-o.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
432
+ const after = fs.readFileSync(path.join(root, 'workflow', 'specs', '2026-10-12-o.md'), 'utf8');
433
+ check(`S19c 待澄清门:${name} → 放行落态`, r.status === 0 && after.includes('状态: approved'),
434
+ JSON.stringify({ status: r.status, stderr: r.stderr }));
435
+ fs.rmSync(root, { recursive: true, force: true });
436
+ }
437
+ // intents 不受本门约束(歧义标记的落点是 spec/plan 的设计判据;intent 是需求单,澄清天然发生在此处)
438
+ const root3 = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl3-'));
439
+ fs.mkdirSync(path.join(root3, 'workflow', 'intents'), { recursive: true });
440
+ fs.writeFileSync(path.join(root3, 'workflow', 'intents', '2026-10-12-p.md'), '---\n状态: draft\n级别: L1\n---\n# I\n\n- 待澄清:范围含不含移动端?\n');
441
+ const r3 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-12-p.md', '--delegated', '可以'], { cwd: root3, encoding: 'utf8' });
442
+ check('S19c 待澄清门:intent 不受本门约束(澄清天然发生在需求单处)',
443
+ r3.status === 0 && fs.readFileSync(path.join(root3, 'workflow', 'intents', '2026-10-12-p.md'), 'utf8').includes('状态: approved'),
444
+ JSON.stringify({ status: r3.status, stderr: r3.stderr }));
445
+ fs.rmSync(root3, { recursive: true, force: true });
446
+ }
447
+
389
448
  // ---- S20【逐阶段门】入口存在但未确认(draft)→ 拒绝 ----
390
449
  {
391
450
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg2-'));