agentic-flow-kit 0.9.1 → 0.9.4

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 (59) hide show
  1. package/README.md +93 -93
  2. package/modules/hosts/claude/commands/wf-build.md +3 -3
  3. package/modules/hosts/claude/commands/wf-design.md +3 -3
  4. package/modules/hosts/claude/commands/wf-new-task.md +13 -2
  5. package/modules/hosts/claude/commands/wf-plan.md +3 -3
  6. package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -1
  7. package/modules/hosts/claude/commands/wf-test.md +1 -1
  8. package/modules/hosts/codex/commands/wf-build.md +3 -3
  9. package/modules/hosts/codex/commands/wf-design.md +3 -3
  10. package/modules/hosts/codex/commands/wf-new-task.md +13 -2
  11. package/modules/hosts/codex/commands/wf-plan.md +3 -3
  12. package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -1
  13. package/modules/hosts/codex/commands/wf-test.md +1 -1
  14. package/modules/hosts/cursor/commands/wf-build.md +3 -3
  15. package/modules/hosts/cursor/commands/wf-design.md +3 -3
  16. package/modules/hosts/cursor/commands/wf-new-task.md +13 -2
  17. package/modules/hosts/cursor/commands/wf-plan.md +3 -3
  18. package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -1
  19. package/modules/hosts/cursor/commands/wf-test.md +1 -1
  20. package/modules/hosts/opencode/commands/wf-build.md +3 -3
  21. package/modules/hosts/opencode/commands/wf-design.md +3 -3
  22. package/modules/hosts/opencode/commands/wf-new-task.md +13 -2
  23. package/modules/hosts/opencode/commands/wf-plan.md +3 -3
  24. package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -1
  25. package/modules/hosts/opencode/commands/wf-test.md +1 -1
  26. package/modules/hosts/trae/commands/wf-build.md +3 -3
  27. package/modules/hosts/trae/commands/wf-design.md +3 -3
  28. package/modules/hosts/trae/commands/wf-new-task.md +13 -2
  29. package/modules/hosts/trae/commands/wf-plan.md +3 -3
  30. package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
  31. package/modules/hosts/trae/commands/wf-test.md +1 -1
  32. package/modules/hosts/trae/rules/project-workflow.md +1 -1
  33. package/package.json +45 -45
  34. package/templates/_agents/commands/build.md +2 -2
  35. package/templates/_agents/commands/design.md +1 -1
  36. package/templates/_agents/commands/plan.md +3 -3
  37. package/templates/_agents/commands/sync-hosts.md +1 -1
  38. package/templates/_agents/scripts/check-lane-surface.mjs +20 -7
  39. package/templates/_agents/scripts/check-lane-surface.test.mjs +24 -2
  40. package/templates/_agents/scripts/check-ledger-invariant.mjs +135 -0
  41. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +161 -0
  42. package/templates/_agents/scripts/check-loop.mjs +58 -15
  43. package/templates/_agents/scripts/check-loop.test.mjs +100 -2
  44. package/templates/_agents/scripts/confirm-doc.mjs +19 -7
  45. package/templates/_agents/scripts/confirm-doc.test.mjs +31 -0
  46. package/templates/_agents/scripts/fill-intent.mjs +1 -1
  47. package/templates/_agents/scripts/fill-plan.test.mjs +4 -3
  48. package/templates/_agents/scripts/gate-checklist.mjs +1 -0
  49. package/templates/_agents/scripts/gen-workflow-metrics.mjs +75 -5
  50. package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +104 -2
  51. package/templates/_agents/scripts/policy.mjs +1 -0
  52. package/templates/_agents/scripts/pre-push.test.mjs +8 -1
  53. package/templates/_agents/scripts/solidify-task.test.mjs +5 -2
  54. package/templates/_agents/scripts/stage-gates.mjs +29 -6
  55. package/templates/_agents/scripts/trust-mode.test.mjs +110 -0
  56. package/templates/_agents/scripts-test-exempt.txt +10 -0
  57. package/templates/_githooks/pre-commit +12 -0
  58. package/templates/workflow/README.md +3 -2
  59. package/templates/workflow/plans/_TEMPLATE.md +1 -1
@@ -572,6 +572,37 @@ const gitHead = (root) => {
572
572
  fs.rmSync(root, { recursive: true, force: true });
573
573
  }
574
574
 
575
+ // ---- S31b【方案 C 分歧双严】--batch 分歧件(级别 L1 / risk_level L2,协议期)→ 泳道 suspect 逐份拒绝(2026-10-02 caliber-convergence)----
576
+ {
577
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt3-'));
578
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
579
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
580
+ fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 2 }));
581
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-02-dv1.md'), '---\n状态: draft\n级别: L1\nrisk_level: L2\n日期: 2026-10-02\n---\n# DV\n');
582
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-02-dv1.md', '--delegated', '批量放行', '--batch'], { cwd: root, encoding: 'utf8' });
583
+ const led = readLedgerOf(root);
584
+ const st = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-10-02-dv1.md'), 'utf8');
585
+ check('S31b --batch 分歧件 → 泳道 suspect 拒(exit 2 + 对齐两字段提示 + 零台账未落盘)',
586
+ r.status === 2 && /泳道「suspect」/.test(r.stderr) && /对齐/.test(r.stderr) && led.length === 0 && st.includes('状态: draft'),
587
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), led }));
588
+ fs.rmSync(root, { recursive: true, force: true });
589
+ }
590
+
591
+ // ---- S31c【方案 C 分歧双严】--auto 分歧件 → suspect 拒(消息锁定,先于 trust 判定短路)----
592
+ {
593
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt4-'));
594
+ fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
595
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
596
+ fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 2 }));
597
+ fs.writeFileSync(path.join(root, '.agents', 'trust-mode.json'), JSON.stringify({ enabled: true, level: 1, name: 'Standard', enabledAt: '2026-10-02T00:00:00.000Z' }));
598
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-02-dv2.md'), '---\n状态: draft\n级别: L1\nrisk_level: L2\n日期: 2026-10-02\n---\n# DV2\n');
599
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-02-dv2.md', '--auto'], { cwd: root, encoding: 'utf8' });
600
+ check('S31c --auto(trust=Standard)分歧件 → 泳道 suspect 拒(消息含 suspect 与对齐指引)',
601
+ r.status !== 0 && /suspect/.test(r.stderr) && /对齐/.test(r.stderr),
602
+ JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300) }));
603
+ fs.rmSync(root, { recursive: true, force: true });
604
+ }
605
+
575
606
  // ---- S32 --batch 组合约束:与 --auto / --to 互斥(exit 1,零落账)----
576
607
  {
577
608
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt3-'));
@@ -1,4 +1,4 @@
1
- // flow-kit fill-intent:起草 intent 草稿(背景与问题/目标/非目标/约束/影响面/触达红线/验收标准 共 7 节)
1
+ // flow-kit fill-intent:起草 intent 草稿(背景与问题/历史教训防复发/目标/非目标/约束/影响面/触达红线/验收标准 8 节正文 + 确认与复核 共 9 节)
2
2
  // 读 workflow/intents/_TEMPLATE.md 的节标题 → 输出符合 frontmatter 受限子集(6 字段,含 risk_level 风险泳道)的草稿到指定文件
3
3
  // 用法:node .agents/scripts/fill-intent.mjs --module pipeline --level L1 --topic "test" --output /tmp/x.md
4
4
  // 零依赖;CLI 解析用最简字符串匹配;不替代 AI 起草——本工具只输出模板与提示句
@@ -124,11 +124,12 @@ const runTool = (root, base, level) => spawnSync(process.execPath,
124
124
  fs.rmSync(root, { recursive: true, force: true });
125
125
  }
126
126
  {
127
- // S10c 探索泳道回落:级别缺失、risk_level=L0 → 放行(risk_level 兜底判道)
127
+ // S10c(2026-10-02 caliber-convergence 方案 C 改判):级别缺失 → suspect,不再回落 risk_level——
128
+ // 豁免仅认两字段一致的 low;缺失须先补「级别」再走起草(fail-closed 就严)。
128
129
  const root = mkws({ 'workflow/intents/2026-09-30-exp0.md': '---\n状态: draft\nrisk_level: L0\n日期: 2026-09-30\n---\n# I\n' });
129
130
  const r = runTool(root, '2026-09-30-exp0', 'L1');
130
- check('S10c 探索泳道回落:级别缺失 + risk_level L0 → 放行',
131
- r.status === 0 && fs.existsSync(path.join(root, 'workflow', 'plans', '2026-09-30-exp0.md')),
131
+ check('S10c 方案 C:级别缺失 + risk_level L0 → suspect 拒(不回落,先补级别)',
132
+ r.status === 2 && !fs.existsSync(path.join(root, 'workflow', 'plans', '2026-09-30-exp0.md')),
132
133
  JSON.stringify({ status: r.status, stderr: r.stderr }));
133
134
  fs.rmSync(root, { recursive: true, force: true });
134
135
  }
@@ -84,6 +84,7 @@ const PAIRS = [
84
84
  { doctor: '7', cl: '17', note: '经 §7 整体运行覆盖(发版提交树上仍未收口的 intent/spec/plan)' },
85
85
  { doctor: '7', cl: '18', note: '经 §7 整体运行覆盖(委派台账对账)' },
86
86
  { doctor: '7', cl: '19', note: '经 §7 整体运行覆盖(逐阶段审计——起草先于入口确认 / 台账审批顺序倒置)' },
87
+ { doctor: '7', cl: '20', note: '经 §7 整体运行覆盖(引擎脚本测试覆盖,2026-10-01 gate-script-test-coverage)' },
87
88
  ];
88
89
 
89
90
  // gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
@@ -168,6 +168,59 @@ const idxText = fs.existsSync(path.join(WORKFLOW, 'INDEX.md')) ? fs.readFileSync
168
168
  const idxBytes = Buffer.byteLength(idxText, 'utf8');
169
169
  const idxActive = Number((idxText.match(/^## 活跃((\d+))/m) || [])[1] ?? NaN);
170
170
 
171
+ // ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics;口径定义式单源 = workflow/specs/2026-10-02-ledger-funnel-metrics.md)----
172
+ // 只读台账一手字段(doc/stage/ts/source),坏行跳过(与检查 15/19 同容错);观测面不挂门禁、不新增文档断言(检查 16 零接触)。
173
+ const LEDGER_P = path.join('.agents', 'confirmations.jsonl');
174
+ function readLedgerRows() {
175
+ if (!fs.existsSync(LEDGER_P)) return [];
176
+ return fs.readFileSync(LEDGER_P, 'utf8').split(/\r?\n/)
177
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
178
+ .filter((e) => e && typeof e === 'object');
179
+ }
180
+
181
+ function funnelFromLedger(entries, month) {
182
+ const byDoc = new Map();
183
+ for (const e of entries) {
184
+ if (typeof e.doc !== 'string' || typeof e.ts !== 'string') continue;
185
+ if (!byDoc.has(e.doc)) byDoc.set(e.doc, []);
186
+ byDoc.get(e.doc).push(e);
187
+ }
188
+ const res = { closed: 0, full: 0, legacy: 0, once: 0, rework: 0, ledgerRows: 0, days: [], items: [] };
189
+ for (const e of entries) if (typeof e.ts === 'string' && e.ts.slice(0, 7) === month) res.ledgerRows++;
190
+ for (const [doc, rs] of byDoc) {
191
+ rs.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
192
+ const nonRevert = rs.filter((r) => !/^revert-/.test(String(r.stage || '')));
193
+ const terminals = nonRevert.filter((r) => r.stage === 'done' || r.stage === 'closed');
194
+ if (!terminals.length) continue; // 未收口件不进漏斗桶(仍计台账行数)
195
+ const lastTerm = terminals[terminals.length - 1];
196
+ if (String(lastTerm.ts).slice(0, 7) !== month) continue; // 月度归桶 = 终态行所在月
197
+ // 确认阶段按件型(复核 P2-3 收口):docs=approved;incidents=fixed(两跳确认门 fixed→closed,
198
+ // 永无 approved 行——按 approved 判会把已走确认门的 incident 误归协议前)
199
+ const isInc = doc.includes('/incidents/');
200
+ const confirmStage = isInc ? 'fixed' : 'approved';
201
+ const confirms = nonRevert.filter((r) => r.stage === confirmStage);
202
+ const hasChain = confirms.length >= 1;
203
+ const hasRevert = rs.some((r) => /^revert-/.test(String(r.stage || '')));
204
+ const kind = !hasChain ? 'legacy' : (hasRevert || confirms.length >= 2 || terminals.length >= 2 ? 'rework' : 'once');
205
+ const firstTs = hasChain ? confirms[0].ts : rs[0].ts; // 协议前件首行起算
206
+ const days = (Date.parse(lastTerm.ts) - Date.parse(firstTs)) / 86400000;
207
+ res.closed++;
208
+ if (kind === 'legacy') res.legacy++;
209
+ else { res.full++; res[kind === 'once' ? 'once' : 'rework']++; }
210
+ if (Number.isFinite(days) && days >= 0) res.days.push(days);
211
+ res.items.push({ doc, kind, days: Number.isFinite(days) && days >= 0 ? days.toFixed(1) : '—' });
212
+ }
213
+ return res;
214
+ }
215
+
216
+ const ledgerRows = readLedgerRows();
217
+ const funnel = funnelFromLedger(ledgerRows, month);
218
+ const sortedDays = [...funnel.days].sort((a, b) => a - b);
219
+ const medianDays = sortedDays.length
220
+ ? (sortedDays.length % 2 ? sortedDays[(sortedDays.length - 1) / 2] : (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2)
221
+ : null;
222
+ const funnelRow = `| ${month} | ${funnel.closed} | ${funnel.full} | ${funnel.legacy} | ${funnel.once} | ${funnel.rework} | ${medianDays === null ? '—' : medianDays.toFixed(1)} | ${funnel.ledgerRows} |`;
223
+
171
224
  const docs = scanDocs();
172
225
  const surf = scanSurface();
173
226
  const row = `| ${month} | ${docs.files}(活跃 ${docs.active} / 终态 ${docs.terminal}) | ${kb(docs.bytes)} | ${docs.withModule}/${docs.files} | ${kb(surf.total)}(峰值 ${surf.peak ? `${surf.peak.pct.toFixed(1)}% ${surf.peak.label}` : '—'}) | ${kb(idxBytes)}(${Number.isNaN(idxActive) ? '—' : idxActive} 行) |`;
@@ -179,9 +232,12 @@ for (const [t, v] of Object.entries(docs.perType)) console.log(` ${t.padEnd(1
179
232
  console.log(` 索引:workflow/INDEX.md ${kb(idxBytes)}(活跃 ${Number.isNaN(idxActive) ? '—' : idxActive} 行)`);
180
233
  for (const e of surf.entries) console.log(` ${e.label.padEnd(44)} ${String(e.max).padStart(6)} / ${String(e.lim).padStart(6)} B(${e.pct.toFixed(1)}%)`);
181
234
  console.log(` 常驻面合计:${kb(surf.total)}`);
235
+ console.log(` 闭环漏斗:收口 ${funnel.closed}(完整链 ${funnel.full} = 一次通过 ${funnel.once} + 返工 ${funnel.rework};协议前 ${funnel.legacy})|中位周期 ${medianDays === null ? '—' : `${medianDays.toFixed(1)} 天`}|台账行 ${funnel.ledgerRows}`);
236
+ for (const it of funnel.items) console.log(` ${String(it.doc).padEnd(58)} ${it.kind.padEnd(7)} ${it.days} 天`);
237
+ if (funnel.items.length) console.log(' ℹ️ 机器口径可与 workflow/delegations.md 自做表的人工自报交叉对照(差异即信号,不对账)');
182
238
  console.log(` ${DRY ? '(--dry-run,未写盘)' : ''}`);
183
239
 
184
- if (DRY) { console.log(`\n将写入行:\n${row}`); process.exit(0); }
240
+ if (DRY) { console.log(`\n将写入行:\n${row}\n${funnelRow}`); process.exit(0); }
185
241
 
186
242
  // ---- 组装:GENERATED 区内的月表按月份键 upsert(同月重跑覆盖,行序按月升序)----
187
243
  const HEADER = `# workflow 规模与规则面月度快照
@@ -193,17 +249,31 @@ const TABLE_HEAD = [
193
249
  '| 月份 | 文档(活跃/终态) | 文档字节 | 模块已填/总 | 常驻面字节(预算峰值) | INDEX(活跃行) |',
194
250
  '|---|---|---|---|---|---|',
195
251
  ];
252
+ // 漏斗表头(第二表;口径定义式单源 = workflow/specs/2026-10-02-ledger-funnel-metrics.md)
253
+ const FUNNEL_HEAD = [
254
+ '## 闭环漏斗(机器口径,源:.agents/confirmations.jsonl——定义式见 specs/2026-10-02-ledger-funnel-metrics.md)',
255
+ '',
256
+ '| 月份 | 收口 | 完整链 | 协议前 | 一次通过 | 返工件 | 中位周期(天) | 台账行数 |',
257
+ '|---|---|---|---|---|---|---|---|',
258
+ ];
196
259
  const existing = fs.existsSync(METRICS_P) ? fs.readFileSync(METRICS_P, 'utf8') : '';
197
260
  let rows = [];
261
+ let funnelRows = [];
198
262
  if (existing.includes(BEGIN) && existing.includes(END)) {
199
- rows = existing.slice(existing.indexOf(BEGIN), existing.indexOf(END))
200
- .split(/\r?\n/)
201
- .filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
263
+ const section = existing.slice(existing.indexOf(BEGIN), existing.indexOf(END));
264
+ const marker = FUNNEL_HEAD[0];
265
+ const bodyPart = section.includes(marker) ? section.slice(0, section.indexOf(marker)) : section;
266
+ const funnelPart = section.includes(marker) ? section.slice(section.indexOf(marker)) : '';
267
+ rows = bodyPart.split(/\r?\n/).filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
268
+ funnelRows = funnelPart.split(/\r?\n/).filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
202
269
  }
203
270
  rows = rows.filter((l) => !l.startsWith(`| ${month} `));
204
271
  rows.push(row);
205
272
  rows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
206
- const inner = `${BEGIN}\n\n${[...TABLE_HEAD, ...rows].join('\n')}\n\n${END}`;
273
+ funnelRows = funnelRows.filter((l) => !l.startsWith(`| ${month} `));
274
+ funnelRows.push(funnelRow);
275
+ funnelRows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
276
+ const inner = `${BEGIN}\n\n${[...TABLE_HEAD, ...rows].join('\n')}\n\n${[...FUNNEL_HEAD, ...funnelRows].join('\n')}\n\n${END}`;
207
277
  let out;
208
278
  if (existing.includes(BEGIN) && existing.includes(END)) {
209
279
  out = existing.slice(0, existing.indexOf(BEGIN)) + inner + existing.slice(existing.indexOf(END) + END.length);
@@ -70,8 +70,12 @@ const readMetrics = (root) => fs.readFileSync(path.join(root, 'workflow', 'metri
70
70
  run(root, ['--month', '2026-09']);
71
71
  run(root, ['--month', '2026-09']);
72
72
  let md = readMetrics(root);
73
- const n09 = (md.match(/^\| 2026-09 \|/gm) || []).length;
74
- check('场景 2:同月重跑只保留一行', n09 === 1, `${n09} 行\n${md}`);
73
+ const funnelMark = '## 闭环漏斗';
74
+ const bodyPart = md.slice(0, md.includes(funnelMark) ? md.indexOf(funnelMark) : undefined);
75
+ const n09 = (bodyPart.match(/^\| 2026-09 \|/gm) || []).length;
76
+ const n09f = ((md.includes(funnelMark) ? md.slice(md.indexOf(funnelMark)) : '').match(/^\| 2026-09 \|/gm) || []).length;
77
+ check('场景 2:同月重跑只保留一行(体量表分段计数)', n09 === 1, `${n09} 行\n${md}`);
78
+ check('场景 2:漏斗表同月 upsert 不重复', n09f === 1, `${n09f} 行\n${md}`);
75
79
  run(root, ['--month', '2026-08']);
76
80
  md = readMetrics(root);
77
81
  const i08 = md.indexOf('| 2026-08 |');
@@ -114,6 +118,104 @@ const readMetrics = (root) => fs.readFileSync(path.join(root, 'workflow', 'metri
114
118
  fs.rmSync(root, { recursive: true, force: true });
115
119
  }
116
120
 
121
+ // ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics;口径定义式 = specs/2026-10-02-ledger-funnel-metrics.md)----
122
+ const LROW = (o = {}) => JSON.stringify({
123
+ ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/intents/demo.md', stage: 'approved',
124
+ fingerprint: 'b'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '确认', ...o,
125
+ });
126
+ // mkfixLedger:在 mkfix 基础上写台账(LROW 已是 JSON 字符串,直接 join——勿再 stringify 双重编码)
127
+ const mkfixLedger = (lines) => {
128
+ const root = mkfix();
129
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), lines.join('\n') + '\n', 'utf8');
130
+ return root;
131
+ };
132
+ {
133
+ // L1:一次通过(approved→done 同月)→ 收口 1 / 完整链 1 / 一次通过 1 / 周期 1.0 天
134
+ const root = mkfixLedger([
135
+ LROW({ ts: '2026-10-01T00:00:00.000Z' }),
136
+ LROW({ ts: '2026-10-02T00:00:00.000Z', stage: 'done', prev: 'approved' }),
137
+ ]);
138
+ const r = run(root, ['--month', '2026-10']);
139
+ const md = readMetrics(root);
140
+ check('漏斗 L1 一次通过:| 2026-10 | 1 | 1 | 0 | 1 | 0 | 1.0 | 2 |',
141
+ r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| 1\.0 \| 2 \|/.test(md), `${r.stdout}${md}`);
142
+ fs.rmSync(root, { recursive: true, force: true });
143
+ }
144
+ {
145
+ // L2:revert 行(回退后重走)→ 返工件
146
+ const root = mkfixLedger([
147
+ LROW({ ts: '2026-10-01T00:00:00.000Z', stage: 'revert-draft', fingerprint: 'n/a', prev: 'approved' }),
148
+ LROW({ ts: '2026-10-02T00:00:00.000Z' }),
149
+ LROW({ ts: '2026-10-03T00:00:00.000Z', stage: 'done', prev: 'approved' }),
150
+ ]);
151
+ const r = run(root, ['--month', '2026-10']);
152
+ const md = readMetrics(root);
153
+ check('漏斗 L2 revert 行 → 返工件 1(周期按首 approved 起算 1.0 天)',
154
+ r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 0 \| 1 \| 1\.0 \| 3 \|/.test(md), `${r.stdout}${md}`);
155
+ fs.rmSync(root, { recursive: true, force: true });
156
+ }
157
+ {
158
+ // L3:重确认(两次 approved)→ 返工件
159
+ const root = mkfixLedger([
160
+ LROW({ ts: '2026-10-01T00:00:00.000Z' }),
161
+ LROW({ ts: '2026-10-02T00:00:00.000Z', quote: '重确认' }),
162
+ LROW({ ts: '2026-10-03T00:00:00.000Z', stage: 'done', prev: 'approved' }),
163
+ ]);
164
+ const r = run(root, ['--month', '2026-10']);
165
+ const md = readMetrics(root);
166
+ check('漏斗 L3 两次 approved → 返工件 1',
167
+ r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 0 \| 1 \| 2\.0 \| 3 \|/.test(md), `${r.stdout}${md}`);
168
+ fs.rmSync(root, { recursive: true, force: true });
169
+ }
170
+ {
171
+ // L4:协议前(首行即终态,无 approved)→ 协议前 1(不计一次通过分母)
172
+ const root = mkfixLedger([
173
+ LROW({ ts: '2026-10-02T00:00:00.000Z', stage: 'done', prev: 'approved' }),
174
+ ]);
175
+ const r = run(root, ['--month', '2026-10']);
176
+ const md = readMetrics(root);
177
+ check('漏斗 L4 协议前 → | 1 | 0 | 1 | 0 | 0 |',
178
+ r.status === 0 && /\| 2026-10 \| 1 \| 0 \| 1 \| 0 \| 0 \| 0\.0 \| 1 \|/.test(md), `${r.stdout}${md}`);
179
+ fs.rmSync(root, { recursive: true, force: true });
180
+ }
181
+ {
182
+ // L5:incident 两跳(fixed→closed = incident 确认链,复核 P2-3 收口)→ 完整链一次通过
183
+ const root = mkfixLedger([
184
+ LROW({ ts: '2026-10-01T00:00:00.000Z', doc: 'workflow/incidents/inc.md', stage: 'fixed' }),
185
+ LROW({ ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/incidents/inc.md', stage: 'closed', prev: 'fixed' }),
186
+ ]);
187
+ const r = run(root, ['--month', '2026-10']);
188
+ const md = readMetrics(root);
189
+ check('漏斗 L5 incident 两跳确认链 → 完整链 1 / 一次通过 1 / 周期 1.0',
190
+ r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| 1\.0 \|/.test(md), `${r.stdout}${md}`);
191
+ fs.rmSync(root, { recursive: true, force: true });
192
+ }
193
+ {
194
+ // L7:incident 单行 closed(存量补关,无确认链)→ 协议前
195
+ const root = mkfixLedger([
196
+ LROW({ ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/incidents/old.md', stage: 'closed', prev: 'fixed' }),
197
+ ]);
198
+ const r = run(root, ['--month', '2026-10']);
199
+ const md = readMetrics(root);
200
+ check('漏斗 L7 incident 单行 closed → 协议前 1',
201
+ r.status === 0 && /\| 2026-10 \| 1 \| 0 \| 1 \| 0 \| 0 \|/.test(md), `${r.stdout}${md}`);
202
+ fs.rmSync(root, { recursive: true, force: true });
203
+ }
204
+ {
205
+ // L6:跨月收口(9 月 approved、10 月 done)→ 归桶 10 月;9 月只计台账行
206
+ const root = mkfixLedger([
207
+ LROW({ ts: '2026-09-30T00:00:00.000Z' }),
208
+ LROW({ ts: '2026-10-02T00:00:00.000Z', stage: 'done', prev: 'approved' }),
209
+ ]);
210
+ const rSep = run(root, ['--month', '2026-09']);
211
+ const rOct = run(root, ['--month', '2026-10']);
212
+ const md = readMetrics(root);
213
+ check('漏斗 L6 跨月收口归桶 10 月(9 月零收口 + 台账行 1)',
214
+ rSep.status === 0 && rOct.status === 0 && /\| 2026-09 \| 0 \| 0 \| 0 \| 0 \| 0 \| — \| 1 \|/.test(md)
215
+ && /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| 2\.0 \| 1 \|/.test(md), `${rSep.stdout}${rOct.stdout}${md}`);
216
+ fs.rmSync(root, { recursive: true, force: true });
217
+ }
218
+
117
219
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
118
220
  if (fail) {
119
221
  console.log(`\n${USAGE}`);
@@ -23,6 +23,7 @@ export const POLICIES = {
23
23
  bindingTs: '2026-09-28',
24
24
  check17UnclosedAfter: '0.8.0',
25
25
  stageGateSince: '2026-09-29', // 检查 19 逐阶段审计的生效日(2026-09-30 stage-gate-machine);锚 = 台账 ts 的 UTC 日期(复核 P2-3:本批 ts 2026-09-29T17:33Z 落在受审范围,顺序合规故静默);v1 无此键 → 该检查整体跳过
26
+ riskLevelSince: '2026-10-01', // intent risk_level 双字段协议生效日(2026-10-01 hybrid-governance 落地 fill-intent 双写;2026-10-02 caliber-convergence 方案 C 引入消费)——此日前建档的 intent 无该字段不判 suspect;v1 无此键 → 双字段协议视为从未生效(单字段判)
26
27
  },
27
28
  };
28
29
 
@@ -12,7 +12,12 @@ import { spawnSync } from 'node:child_process';
12
12
  import { fileURLToPath } from 'node:url';
13
13
 
14
14
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
15
- const PRE_PUSH = path.join(SCRIPT_DIR, '..', '..', '_githooks', 'pre-push');
15
+ // 钩子双布局探测:templates 布局(../../_githooks)与装户布局(../../.githooks)——本测试随 sync
16
+ // 落到 .agents/scripts 后 ../../_githooks 不存在(装户钩子在仓库根 .githooks/),2026-10-02 ci-red-batch
17
+ const PRE_PUSH = [
18
+ path.join(SCRIPT_DIR, '..', '..', '_githooks', 'pre-push'),
19
+ path.join(SCRIPT_DIR, '..', '..', '.githooks', 'pre-push'),
20
+ ].find((p) => fs.existsSync(p));
16
21
 
17
22
  let pass = 0;
18
23
  let fail = 0;
@@ -31,6 +36,8 @@ const setup = () => {
31
36
  fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
32
37
  // 挂载真实 pre-push(装户同款),stub 替换 check-loop 记录 argv
33
38
  fs.copyFileSync(PRE_PUSH, path.join(root, '.githooks', 'pre-push'));
39
+ // Linux:git 静默跳过不可执行钩子(推送照常成功、桩无记录)——挂载后必须设执行位;桩经 sh 调用无须 +x
40
+ fs.chmodSync(path.join(root, '.githooks', 'pre-push'), 0o755);
34
41
  fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), '#!/bin/sh\necho "$@" >> .stub-args\nexit 0\n');
35
42
  fs.writeFileSync(path.join(root, 'a.txt'), 'a\n');
36
43
  git(root, ['init', '-q']);
@@ -50,6 +50,9 @@ const ledgerLines = (root) => {
50
50
  const p = path.join(root, '.agents', 'confirmations.jsonl');
51
51
  return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
52
52
  };
53
+ // 迁移目标名含当天日期前缀(solidify 以 toISOString 生成)——动态取值防跨天脆断
54
+ const TODAY = new Date().toISOString().slice(0, 10);
55
+ const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-demo-task.md`);
53
56
 
54
57
  // T1:默认只迁移不落账
55
58
  {
@@ -58,7 +61,7 @@ const ledgerLines = (root) => {
58
61
  const r = run(d, ['--topic', 'demo', '--root', d]);
59
62
  check('T1 默认迁移成功 exit 0', r.code === 0, r.out);
60
63
  check('T1 草稿已迁移到 intents 且日期前缀正确',
61
- fs.existsSync(path.join(d, 'workflow', 'intents', '2026-10-01-demo-task.md')), '');
64
+ fs.existsSync(migratedPath(d)), '');
62
65
  check('T1 零台账新增(无确认来源绝不落账)', ledgerLines(d).length === 0, JSON.stringify(ledgerLines(d)));
63
66
  check('T1 输出含确认指引', r.out.includes('确认指引') && r.out.includes('--delegated'), r.out);
64
67
  }
@@ -73,7 +76,7 @@ const ledgerLines = (root) => {
73
76
  check('T2 quote 原样(脚本不编造话术)',
74
77
  lines[0].includes('"quote":"固化了可以"') && lines[0].includes('chat-delegated'), lines[0]);
75
78
  check('T2 目标文档状态已 approved',
76
- fs.readFileSync(path.join(d, 'workflow', 'intents', '2026-10-01-demo-task.md'), 'utf8').includes('状态: approved'), '');
79
+ fs.readFileSync(migratedPath(d), 'utf8').includes('状态: approved'), '');
77
80
  }
78
81
  // T3:--auto 在 Strict(缺 trust-mode.json)下被拒 → 失败计数并 exit 1
79
82
  {
@@ -119,7 +119,7 @@ export function entryConfirmed(root, base) {
119
119
  const intentAbs = path.join(root, intentRel);
120
120
  if (fs.existsSync(intentAbs)) {
121
121
  const r = judgeDoc(root, intentAbs, intentRel, ENTRY_STATUSES);
122
- return { ...r, kind: 'intent', rel: intentRel, level: readFm(intentAbs, '级别'), risk: readFm(intentAbs, 'risk_level'), storageLegacy: entryStorageLegacy(root, intentAbs) };
122
+ return { ...r, kind: 'intent', rel: intentRel, level: readFm(intentAbs, '级别'), risk: readFm(intentAbs, 'risk_level'), date: readFm(intentAbs, '日期'), storageLegacy: entryStorageLegacy(root, intentAbs) };
123
123
  }
124
124
  const incRel = `workflow/incidents/${base}.md`;
125
125
  const incAbs = path.join(root, incRel);
@@ -143,15 +143,38 @@ export function specConfirmed(root, base) {
143
143
  return judgeDoc(root, abs, rel, ENTRY_STATUSES);
144
144
  }
145
145
 
146
+ // laneOfEntry / laneOfDoc:泳道单源取法(2026-10-02 caliber-convergence 方案 C「分歧双严」)
147
+ // risk_level 为 intent 专属协议字段(fill-intent 双写),生效锚 policy.riskLevelSince:
148
+ // 级别非法/缺失 → suspect(fail-closed 就严);
149
+ // 日期 ≥ 锚(双字段协议期):risk 合法且与级别一致 → 按值判 low/high,否则(缺失/非法/分歧)→ suspect;
150
+ // 日期 < 锚(协议前存量):两值俱在且分歧 → suspect,否则按级别单字段判(risk 缺席不惩罚——字段协议尚不存在)。
151
+ // suspect 消费规则(双严):红线类检查按 low 执行(拦红线);豁免类门(起草豁免/--batch/--auto/加固门
152
+ // lane/泳道豁免/逐阶段深度)按 high 执行(不豁免)。取代原三处并存语义(任一低即低 / 级别优先回落 / 仅级别)。
153
+ export function laneOfEntry(level, risk, riskLevelSince, date) {
154
+ const L = /^L[0-3]$/.test(level || '') ? level : '';
155
+ if (!L) return 'suspect';
156
+ const R = /^L[0-3]$/.test(risk || '') ? risk : '';
157
+ const dualProtocol = !!riskLevelSince && /^\d{4}-\d{2}-\d{2}$/.test(date || '') && date >= riskLevelSince;
158
+ if (dualProtocol) return (R && R === L) ? ((L === 'L0' || L === 'L1') ? 'low' : 'high') : 'suspect';
159
+ if (R && R !== L) return 'suspect';
160
+ return (L === 'L0' || L === 'L1') ? 'low' : 'high';
161
+ }
162
+
163
+ // laneOfDoc:非 intent 件(spec/plan/incident——级别单字段协议):合法按值判,非法 → suspect。
164
+ export function laneOfDoc(level) {
165
+ if (!/^L[0-3]$/.test(level || '')) return 'suspect';
166
+ return (level === 'L0' || level === 'L1') ? 'low' : 'high';
167
+ }
168
+
146
169
  // draftGateFor:起草门总判(kind = 'spec' | 'plan',fill 工具与 confirm-doc 逐阶段门共用)
147
170
  export function draftGateFor(kind, root, base) {
148
171
  const entry = entryConfirmed(root, base);
149
172
  if (!entry.ok) {
150
- // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening,口径见头部注释):L0/L1 draft intent
151
- // 放行起草——「先动手后确认」;泳道取级别优先、缺失/非法回落 risk_level(与 fill-intent 双写兼容,
152
- // 级别合法但为 L2/L3 时不回落——fail-closed 就严)。
153
- const lane = /^L[0-3]$/.test(entry.level || '') ? entry.level : entry.risk;
154
- if (entry.kind === 'intent' && entry.status === 'draft' && (lane === 'L0' || lane === 'L1')) {
173
+ // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent 放行起草
174
+ // ——「先动手后确认」;泳道取法自 2026-10-02 caliber-convergence 起单源 laneOfEntry(方案 C 分歧
175
+ // 双严:suspect 不豁免——级别/risk_level 缺失、非法或分歧均按防御道走入口确认)。
176
+ const lane = laneOfEntry(entry.level, entry.risk, loadKitPolicy(root).riskLevelSince, entry.date);
177
+ if (entry.kind === 'intent' && entry.status === 'draft' && lane === 'low') {
155
178
  return { ok: true, entry, laneBypass: true };
156
179
  }
157
180
  return {
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ // trust-mode.test.mjs — AI 自治信任等级 fixture 套件(2026-10-01 gate-script-test-coverage)
3
+ // 覆盖:三级语义(Strict/Standard/Trusted 名称↔数值双轨)、缺文件 fail-closed 回落 Strict、
4
+ // --enable/--disable/--status CLI 行为、非法 --level 拒绝、与 confirm-doc --auto 的端到端
5
+ // (Strict 缺文件拒绝不落账 / Trusted 放行 source=ai-auto-trust-L2)。
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
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
+ const TRUST = path.join(SCRIPT_DIR, 'trust-mode.mjs');
14
+ const CONFIRM = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
15
+
16
+ let pass = 0;
17
+ let fail = 0;
18
+ const check = (desc, cond, detail = '') => {
19
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
20
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
21
+ };
22
+
23
+ const mkfix = () => {
24
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'trust-test-'));
25
+ for (const s of ['intents', 'plans', 'specs', 'incidents']) {
26
+ fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
27
+ }
28
+ fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
29
+ fs.mkdirSync(path.join(d, '.zcode'), { recursive: true });
30
+ fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'),
31
+ ['doc.status.all=draft approved done superseded cancelled', 'doc.status.confirmed=approved done superseded cancelled',
32
+ 'doc.status.active=draft approved', 'doc.status.terminal=done superseded cancelled', 'doc.status.abandoned=superseded cancelled',
33
+ 'incident.status.all=open fixed closed', 'incident.status.active=open', 'level.all=L0 L1 L2 L3'].join('\n') + '\n');
34
+ fs.writeFileSync(path.join(d, '.agents', 'workflow-modules.txt'), '# modules\npipeline\nui\n');
35
+ return d;
36
+ };
37
+ const runTrust = (root, args) => {
38
+ const r = spawnSync(process.execPath, [TRUST, ...args], { cwd: root, encoding: 'utf8' });
39
+ return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
40
+ };
41
+ const cfg = (root) => {
42
+ const p = path.join(root, '.agents', 'trust-mode.json');
43
+ return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
44
+ };
45
+ const ledgerLines = (root) => {
46
+ const p = path.join(root, '.agents', 'confirmations.jsonl');
47
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
48
+ };
49
+ const DRAFT = (level = 'L1') => `---\n状态: draft\n级别: ${level}\nrisk_level: ${level}\n日期: 2026-10-01\n模块: pipeline\n---\n# INTENT — demo\n`;
50
+
51
+ // T1:缺文件 → fail-closed Strict 默认
52
+ {
53
+ const d = mkfix();
54
+ const r = runTrust(d, ['--status']);
55
+ check('T1 缺文件 --status 输出 Strict/level0/enabled false', r.code === 0 && r.out.includes('"name": "Strict"') && r.out.includes('"level": 0') && r.out.includes('"enabled": false'), r.out);
56
+ }
57
+ // T2:三级语义映射正确
58
+ {
59
+ check('T2 名称↔数值双轨(NAME_BY_LEVEL 内容)',
60
+ fs.readFileSync(TRUST, 'utf8').includes("NAME_BY_LEVEL = { 0: 'Strict', 1: 'Standard', 2: 'Trusted' }"), '');
61
+ }
62
+ // T3:--enable --level Trusted → 落盘 enabled true/level 2/name Trusted
63
+ {
64
+ const d = mkfix();
65
+ const r = runTrust(d, ['--enable', '--level', 'Trusted']);
66
+ const c = cfg(d);
67
+ check('T3 --level Trusted 落盘正确', r.code === 0 && c && c.enabled === true && c.level === 2 && c.name === 'Trusted', JSON.stringify(c));
68
+ }
69
+ // T4:--enable --level 0(Strict 数字)→ 关闭
70
+ {
71
+ const d = mkfix();
72
+ const r = runTrust(d, ['--enable', '--level', '0']);
73
+ const c = cfg(d);
74
+ check('T4 --level 0 → 关闭 Strict', r.code === 0 && c && c.enabled === false && c.level === 0 && c.name === 'Strict', JSON.stringify(c));
75
+ }
76
+ // T5:非法 --level → 拒绝 exit 1
77
+ {
78
+ const d = mkfix();
79
+ const r = runTrust(d, ['--enable', '--level', 'Bogus']);
80
+ check('T5 非法 --level 拒绝 exit 1', r.code === 1 && r.out.includes('必须为'), r.out);
81
+ }
82
+ // T6:--disable → 关闭 Strict
83
+ {
84
+ const d = mkfix();
85
+ runTrust(d, ['--enable', '--level', 'Trusted']);
86
+ const r = runTrust(d, ['--disable']);
87
+ const c = cfg(d);
88
+ check('T6 --disable 复位 Strict', r.code === 0 && c && c.enabled === false && c.level === 0 && c.name === 'Strict', JSON.stringify(c));
89
+ }
90
+ // T7 e2e:Strict(缺文件)下 confirm-doc --auto 拒绝不落账
91
+ {
92
+ const d = mkfix();
93
+ fs.writeFileSync(path.join(d, 'workflow', 'intents', '2026-10-01-demo.md'), DRAFT('L1'));
94
+ const r = spawnSync(process.execPath, [CONFIRM, 'workflow/intents/2026-10-01-demo.md', '--root', d, '--auto'], { cwd: d, encoding: 'utf8' });
95
+ check('T7 Strict 下 --auto 拒绝且零台账', r.status !== 0 && ledgerLines(d).length === 0, `${r.stdout}${r.stderr}\n${ledgerLines(d).join('|')}`);
96
+ }
97
+ // T8 e2e:Trusted 下 confirm-doc --auto 放行 L1,台账 source=ai-auto-trust-L2
98
+ {
99
+ const d = mkfix();
100
+ fs.writeFileSync(path.join(d, 'workflow', 'intents', '2026-10-01-demo.md'), DRAFT('L1'));
101
+ runTrust(d, ['--enable', '--level', 'Trusted']);
102
+ const r = spawnSync(process.execPath, [CONFIRM, 'workflow/intents/2026-10-01-demo.md', '--root', d, '--auto'], { cwd: d, encoding: 'utf8' });
103
+ const lines = ledgerLines(d);
104
+ check('T8 Trusted 下 --auto 放行并记 ai-auto-trust-L2',
105
+ r.status === 0 && lines.length === 1 && lines[0].includes('ai-auto-trust-L2') && lines[0].includes('"stage":"approved"'),
106
+ `${r.stdout}${r.stderr}\n${lines.join('|')}`);
107
+ }
108
+
109
+ console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
110
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,10 @@
1
+ # 引擎脚本测试覆盖豁免登记(单源:本文件——check-loop 检查 20 共读,经 sync 下发装副本)
2
+ # 格式:<脚本名> | <豁免理由> | <覆盖证据(套件名;无自动化覆盖如实写「无」)>
3
+ # 语义:登记 ≠ 免责——须写明理由与覆盖证据;无覆盖的登记是「工具/启动器豁免」,后续批次评估补测。
4
+ # 新增引擎脚本默认必须带同名 .test.mjs(README 规范条目);确属库件/工具才登记本文件。
5
+ check-metric-claims.mjs | 经 check-loop.mjs 消费 | 由 check-loop.test.mjs 间接覆盖其调用路径
6
+ ensure-board.mjs | 看板启动器(需真实端口/进程环境) | 无(工具豁免,后续批次评估补测)
7
+ policy.mjs | 库件 | 被 check-loop.test.mjs 直接 import 覆盖
8
+ stage-gates.mjs | 库件 | 被 check-loop.test.mjs 直接 import 覆盖(fill-*/confirm-doc.test 消费其行为)
9
+ verify-wiki-consistency.mjs | wiki 一致性校验工具 | 无(工具豁免,后续批次评估补测)
10
+ wiki-search.mjs | 检索库件 | 无(工具豁免,后续批次评估补测)
@@ -93,6 +93,18 @@ if [ -d templates/_agents ] && [ -f .agents/scripts/check-ledger.mjs ]; then
93
93
  fi
94
94
  fi
95
95
 
96
+ # 确认台账不变量前置(--staged,2026-10-02 ledger-ci-invariant):暂存的 confirmations.jsonl 必须
97
+ # 是 HEAD 的逐字节前缀扩展(append-only)——本地提交时刻即拦「顺手重写台账」,CI 历史全扫兜底。
98
+ # guard 旧装副本自然跳过;暂存未触台账静默。
99
+ if [ -f .agents/scripts/check-ledger-invariant.mjs ] && command -v node >/dev/null 2>&1; then
100
+ if git -c core.quotepath=off diff --cached --name-only | grep -q '^\.agents/confirmations\.jsonl$'; then
101
+ if ! node .agents/scripts/check-ledger-invariant.mjs --staged; then
102
+ echo "pre-commit: 确认台账不变量未通过,提交已阻止——历史行禁删改(append-only),修正走追加补偿行" >&2
103
+ exit 1
104
+ fi
105
+ fi
106
+ fi
107
+
96
108
  # 项目门禁挂载点:.agents/hooks/local-pre-commit(项目自建门禁/门禁模块接线处,存在即执行)
97
109
  if [ -f .agents/hooks/local-pre-commit ]; then
98
110
  run_gate "项目门禁(local-pre-commit)" .agents/hooks/local-pre-commit
@@ -53,8 +53,9 @@
53
53
  3. **确认留痕**:approved / done / fixed / closed 须经 `confirm-doc.mjs`,指纹与台账一致。`--delegated` 原话仅限用户对话内明确确认的当次措辞——官方脚本/门禁不得自动代录或编造 quote(2026-10-01 v09-review-defects:solidify-task 曾自动伪造原话,已修复并纳入回归)。
54
54
  4. **敏感信息**:pre-commit 的 commit-check 扫描密钥。
55
55
  5. **双源与台账**:doctor 核对 managed / owned 的 sha;引擎改动改包源 `templates/` 再 `sync`。
56
+ 6. **台账不可变**(2026-10-02 ledger-ci-invariant):`confirmations.jsonl` 历史行禁删改(append-only 前缀不变量,机器门 = CI 历史全扫 + pre-commit `--staged` 前置);误写/修正只能**追加补偿行**,篡改历史属审计事件(`git revert` 整个提交后重新追加)。
56
57
 
57
- 其余检查(占位符、引用、模块字段、常驻面预算、量化签名、阶段索引、适配器断线)只在 `audit` 不是 `false` 时出警告。
58
+ 其余检查(占位符、引用、模块字段、常驻面预算、量化签名、阶段索引、适配器断线、引擎脚本测试覆盖(检查 20,包源环境——新增引擎脚本默认必须带 `.test.mjs`,库件/工具须登记 `.agents/scripts-test-exempt.txt`,2026-10-01 gate-script-test-coverage))只在 `audit` 不是 `false` 时出警告。
58
59
 
59
60
  **发版草稿**:最近一次改动 `package.json` 的 `version` 的那次提交里,当时已经是 draft 或 approved 的 intent、spec、plan,若被扫描的树上仍是 draft 或 approved,则阻断。当时已是 draft 的不看版本锚。当时已是 approved 的,只在发版版本大于 `policy.mjs` 的 `check17UnclosedAfter` 时阻断;版本 1 没有该键。不带 `--rev` 时现在的状态读工作区,带 `--rev` 时读被推送的那棵树。那次提交之后新建的文件不在范围内。状态为 open 的 incident 不在此列。
60
61
 
@@ -63,7 +64,7 @@
63
64
  ## 闭环规则
64
65
 
65
66
  1. L1 以上新需求始于已确认的 intent;L1 以上修复始于已确认的 incident 草稿(作为 intent 等价入口)
66
- 2. L2 / L3 变更必须先有与入口文档同名的 spec 确认通过(L3 加新会话独立复核),方可起草 plan;L1 用极简 plan——改动面 + 验证方式两节起步,多文件多步骤再加任务拆解/执行顺序。**逐阶段前置已机器强制**(2026-09-30 stage-gate-machine):fill-spec / fill-plan / confirm-doc 校验入口与 spec 前置未过即拒;顺序审计 = check-loop 检查 19
67
+ 2. L2 / L3 变更必须先有与入口文档同名的 spec 确认通过(L3 加新会话独立复核),方可起草 plan;L1 用 Quick-Plan 极简三节——改动方案 / 约束与风险 / 验证计划起步(fill-plan.mjs 单源),多文件多步骤再加任务拆解/执行顺序。**逐阶段前置已机器强制**(2026-09-30 stage-gate-machine):fill-spec / fill-plan / confirm-doc 校验入口与 spec 前置未过即拒;顺序审计 = check-loop 检查 19
67
68
  3. 实现产物必须通过静态门(构建 + 测试 + 项目门禁)
68
69
  4. 线上 / 实测缺陷回落到 `incidents/`,复盘三件套(新 intent、防复发验证、规范条目)缺一不可;**根因属「门禁缺位 / 规范未落地 / 系统性问题」时,即使结构性修复已完成也必须立新 intent** 追踪系统性改进,禁止以「修复已完成」为由绕过 intent 回路
69
70
  5. 关单在 test:逐条勾验入口文档「验收标准」并补证据后 intent → done(不依赖是否上 prod)。done 仍有未勾项会被 check-loop 拦(新建 intent hard-block)。上 prod 另走 deploy(tag / 回滚 / 观察)
@@ -6,7 +6,7 @@
6
6
  # PLAN — <与入口文档同名主题>
7
7
  <!-- 与 intents/ 或 incidents/ 下同名入口文档配对;L2/L3 必须先有 ../specs/ 同名 spec 确认通过;AI 起草、用户对话内确认后开工 -->
8
8
  <!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled(严格枚举,附注写备注键);级别∈L0/L1/L2/L3;正文不再写状态/级别行 -->
9
- <!-- L1 极简形态(2026-09-13 瘦身,L1 默认):单/少文件微改动只填「改动面」+「验证方式」两节——改动面逐条列改哪个文件做什么(判据细节直接写进条目),验证一行静态门 + 按需 UI 实测;任务拆解/执行顺序仅 L1 多文件多步骤时保留 -->
9
+ <!-- L1 极简形态(Quick-Plan,与 fill-plan.mjs 单源一致):单/少文件微改动只填「改动方案」+「约束与风险」+「验证计划」三节——改动方案逐条列改哪个文件做什么(判据细节直接写进条目),验证计划一行静态门 + 按需 UI 实测;任务拆解/执行顺序仅 L1 多文件多步骤时保留 -->
10
10
 
11
11
  对应入口:../intents/YYYY-MM-DD-<主题>.md 或 ../incidents/YYYY-MM-DD-<主题>.md(保留实际一项)
12
12
  对应 spec:../specs/YYYY-MM-DD-<主题>.md(L1 可省略)