agentic-flow-kit 1.8.0 → 1.8.2

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 (49) hide show
  1. package/modules/hosts/claude/commands/wf-new-task.md +1 -0
  2. package/modules/hosts/claude/commands/wf-review.md +1 -1
  3. package/modules/hosts/codex/commands/wf-new-task.md +1 -0
  4. package/modules/hosts/codex/commands/wf-review.md +1 -1
  5. package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
  6. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  7. package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
  8. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  9. package/modules/hosts/trae/commands/wf-new-task.md +1 -0
  10. package/modules/hosts/trae/commands/wf-review.md +1 -1
  11. package/package.json +1 -1
  12. package/templates/_agents/commands/new-task.md +1 -0
  13. package/templates/_agents/commands/review.md +1 -1
  14. package/templates/_agents/engine-lock.json +28 -20
  15. package/templates/_agents/hooks/commit-check.cjs +57 -30
  16. package/templates/_agents/notes/engine-internals.md +17 -0
  17. package/templates/_agents/rule-budgets.txt +7 -1
  18. package/templates/_agents/scripts/agg-judgments.mjs +77 -1
  19. package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
  20. package/templates/_agents/scripts/archive-sweep.mjs +3 -1
  21. package/templates/_agents/scripts/check-convergence.mjs +8 -3
  22. package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
  23. package/templates/_agents/scripts/check-lane-surface.test.mjs +16 -2
  24. package/templates/_agents/scripts/check-ledger-invariant.mjs +30 -4
  25. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +28 -0
  26. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
  27. package/templates/_agents/scripts/check-loop.mjs +9 -0
  28. package/templates/_agents/scripts/check-loop.test.mjs +17 -5
  29. package/templates/_agents/scripts/check-prejudgment.mjs +8 -3
  30. package/templates/_agents/scripts/closeout-l1.mjs +217 -0
  31. package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
  32. package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
  33. package/templates/_agents/scripts/confirm-doc.mjs +14 -2
  34. package/templates/_agents/scripts/confirm-doc.test.mjs +43 -0
  35. package/templates/_agents/scripts/gen-workflow-metrics.mjs +17 -0
  36. package/templates/_agents/scripts/ledger-shards.mjs +42 -5
  37. package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
  38. package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
  39. package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
  40. package/templates/_agents/scripts/pre-push.test.mjs +26 -10
  41. package/templates/_agents/scripts/rule-budget.mjs +14 -1
  42. package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
  43. package/templates/_agents/scripts/verify.mjs +32 -8
  44. package/templates/_agents/scripts/verify.test.mjs +29 -0
  45. package/templates/_agents/scripts-test-exempt.txt +2 -1
  46. package/templates/_agents/skills/review-method/SKILL.md +55 -0
  47. package/templates/_githooks/pre-commit +9 -4
  48. package/templates/_githooks/pre-merge-commit +4 -3
  49. package/templates/_githooks/pre-push +129 -75
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // agg-judgments.test.mjs — 判定流水聚合纯函数套件(2026-10-11 judgment-ledger)
3
3
  // aggFromRows 进程内断言:按月分桶 / --month 过滤 / WARN·BLOCK 计数 / 坏 rules 字段容忍。
4
- import { aggFromRows } from './agg-judgments.mjs';
4
+ // retireCandidates(2026-10-15 减重手段① 自源仓回灌):零命中窗口候选 / 数据不充分守卫 / 断档月不按零计。
5
+ import { aggFromRows, retireCandidates } from './agg-judgments.mjs';
5
6
 
6
7
  let pass = 0;
7
8
  let fail = 0;
@@ -41,5 +42,52 @@ const ROWS = [
41
42
  check('S3 空行数组 → 空 byMonth', Object.keys(aggFromRows([]).byMonth).length === 0);
42
43
  check('S3 null 入参不 throw', Object.keys(aggFromRows(null).byMonth).length === 0);
43
44
 
45
+ // ---- S4 retireCandidates:零命中窗口出候选(6 个数据月,2 条规则) ----
46
+ {
47
+ const mk = (ym, rule, level, count) => ({ ts: `${ym}-15T00:00:00Z`, mode: 'hard', rules: [{ rule, level, count }] });
48
+ const rows = [
49
+ // 规则甲:2026-03 有命中(窗口 04..09 之外),其后零命中 → 候选(全历史 3)
50
+ mk('2026-03', '规则甲', 'WARN', 3),
51
+ // 规则乙:尾部窗口内 2026-08 仍有命中 → 在役
52
+ mk('2026-08', '规则乙', 'BLOCK', 1),
53
+ // 规则丙:仅 2026-03 出现过(窗口外)→ 候选(全历史 5,末次 2026-03)
54
+ mk('2026-03', '规则丙', 'WARN', 5),
55
+ // 6 个数据月铺满窗口 2026-04..2026-09(每月至少一次运行)
56
+ ...['2026-04', '2026-05', '2026-06', '2026-07', '2026-08', '2026-09'].map((ym) => mk(ym, '规则甲', 'WARN', 0)),
57
+ ];
58
+ const r = retireCandidates(rows, 6);
59
+ check('S4 sufficient 且窗口起止正确', r.sufficient === true && r.windowFrom === '2026-04' && r.windowTo === '2026-09', JSON.stringify({ from: r.windowFrom, to: r.windowTo }));
60
+ const cand = r.candidates.map((c) => c.rule);
61
+ check('S4 规则甲/丙为候选、乙在役', cand.includes('规则甲') && cand.includes('规则丙') && !cand.includes('规则乙') && r.recentHits.some((x) => x.rule === '规则乙'), JSON.stringify(cand));
62
+ check('S4 候选按全历史命中升序(甲3 < 丙5)', cand[0] === '规则甲' && cand[1] === '规则丙', JSON.stringify(cand));
63
+ const jia = r.candidates.find((c) => c.rule === '规则甲');
64
+ const bing = r.candidates.find((c) => c.rule === '规则丙');
65
+ check('S4 全历史命中与末次命中月份', jia.total === 3 && jia.lastHit === '2026-03' && bing.total === 5 && bing.lastHit === '2026-03', JSON.stringify({ jia, bing }));
66
+ }
67
+
68
+ // ---- S5 数据不充分守卫:数据月 < 窗口 → 不产候选 ----
69
+ {
70
+ const rows = [{ ts: '2026-10-01T00:00:00Z', mode: 'hard', rules: [{ rule: '规则甲', level: 'WARN', count: 1 }] }];
71
+ const r = retireCandidates(rows, 6);
72
+ check('S5 数据月 1 < 6 → sufficient=false 零候选', r.sufficient === false && r.dataMonths === 1 && r.candidates.length === 0, JSON.stringify(r));
73
+ }
74
+
75
+ // ---- S6 断档月不按零命中计:窗口取「有数据的月」(末 N 个数据月),断档月不参与 ----
76
+ {
77
+ const mk = (ym, rule, count) => ({ ts: `${ym}-15T00:00:00Z`, mode: 'hard', rules: count ? [{ rule, level: 'WARN', count }] : [] });
78
+ // 数据月仅 2026-05 / 2026-08 / 2026-09(06-07 断档无运行);规则甲 05 有命中、08/09 无 → 窗口=末 2 个数据月(08..09)
79
+ const rows = [mk('2026-05', '规则甲', 2), mk('2026-08', '规则甲', 0), mk('2026-09', '规则甲', 0)];
80
+ const r = retireCandidates(rows, 2);
81
+ check('S6 窗口取末 2 个数据月(08..09,断档月跳过),甲窗口外命中 → 候选', r.sufficient === true && r.windowFrom === '2026-08' && r.windowTo === '2026-09' && r.candidates.some((c) => c.rule === '规则甲'), JSON.stringify(r));
82
+ }
83
+
84
+ // ---- S7 窗口边界:末月有命中不算候选 ----
85
+ {
86
+ const mk = (ym, count) => ({ ts: `${ym}-15T00:00:00Z`, mode: 'hard', rules: count ? [{ rule: '规则乙', level: 'WARN', count }] : [] });
87
+ const rows = [...['2026-04', '2026-05', '2026-06', '2026-07', '2026-08'].map((ym) => mk(ym, 0)), mk('2026-09', 1)];
88
+ const r = retireCandidates(rows, 6);
89
+ check('S7 末月命中 → 在役非候选', r.sufficient === true && !r.candidates.some((c) => c.rule === '规则乙') && r.recentHits.some((x) => x.rule === '规则乙'), JSON.stringify(r.candidates));
90
+ }
91
+
44
92
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
45
93
  process.exit(fail ? 1 : 0);
@@ -111,7 +111,9 @@ export function buildDocList(root, dirs = DIRS) {
111
111
  return docs;
112
112
  }
113
113
 
114
- function loadLedgerTs(root) {
114
+ // loadLedgerTs(root):每篇文档的终态台账行最新日期(YYYY-MM-DD)。主台账+分片统一视图;
115
+ // 2026-10-15 起导出(gen-workflow-metrics 归档积压提醒复用——新消费方零复制)。
116
+ export function loadLedgerTs(root) {
115
117
  const out = {};
116
118
  // 台账统一视图(主台账+分片透明合并,坏行跳过)。源仓走 ledger-loader.loadAllConfirmations
117
119
  // (同 doc|stage|fingerprint 键去重取末次);kit 读侧单源 = readLedgerRows(不去重)。对本判据
@@ -26,7 +26,11 @@ const CMD_RE = /npm\s+(run\s+)?test|verify\.mjs/;
26
26
  const CITE_RE = /npm\s+(?:run\s+)?test|verify\.mjs/g;
27
27
  // verify.mjs 全绿行(verifications.jsonl 绿凭证)实际覆盖的命令。不在此集的白名单命令没有对应绿记录。
28
28
  const GATE_CMD = new Set(['npm test', 'npm run test', 'verify.mjs']);
29
- const PATH_RE = /[\w.@\-\\/]+\.(mjs|js|ts|tsx|cs|md|json|html|sql)/g;
29
+ // 路径提取(2026-10-16 自源仓 13f67ef7 回灌):路径体 = ASCII 路径字符 + 字母(中文目录放行),
30
+ // 扩展名加 .vue。单字符类 + 前置分隔锚,避免 (A|B)+ 在长说明上回溯;标点(如路径后的「(说明)」)
31
+ // 不在类内,仍截断。matchAll 取捕获组(剥前导分隔符),路径分隔符归一 posix。
32
+ const PATH_EXT = 'mjs|js|ts|tsx|vue|cs|md|json|html|sql';
33
+ const PATH_RE = new RegExp(`(?:^|[\\s\`'"((])([\\w.@\\-\\\\/\\p{L}]+\\.(?:${PATH_EXT}))`, 'gu');
30
34
  const SEC_RE = /^##\s+.*验收判据/;
31
35
  // 与 gen-verify-evidence.mjs 数据行同形态(kit 六列:短 SHA | ISO 时间 | 门数 | 套件计数 | 绑定单 | runId)。
32
36
  // 只取日期,不复述门数。
@@ -102,7 +106,8 @@ export function run(ctx) {
102
106
  if (hard && CMD_RE.test(ev)) ok = commandCovered(ev, d, greenDates);
103
107
  else if (CMD_RE.test(ev)) ok = true; // advisory:命令白名单认声明
104
108
  else {
105
- const cands = ev.match(PATH_RE) || [];
109
+ const cands = [];
110
+ for (const m of ev.matchAll(PATH_RE)) cands.push(m[1].replace(/\\/g, '/'));
106
111
  ok = cands.some((p) => fs.existsSync(path.join(ROOT, p)));
107
112
  }
108
113
  }
@@ -110,7 +115,7 @@ export function run(ctx) {
110
115
  }
111
116
  if (missed.length) {
112
117
  const detail = `${base} ${rows.length} 条判据中 ${missed.length} 条证据无效(${missed.join('; ')})`;
113
- if (hard) blockers.push(`- [收敛未覆盖] ${detail}——状态列自报不作数;命令须是全量门(npm test / verify.mjs)且 workflow/verify-evidence.md 有不早于本 spec 日期的全绿行;路径只认存在(检查 23)。若本 spec 已标 done:先标后补会与本门互为前提(verify 门序含 check-loop)——解锁序:状态临时改回非 done → 跑 node .agents/scripts/verify.mjs 全量门(全绿尾部自动重生成 workflow/verify-evidence.md 全绿行)→ 再标 done`);
118
+ if (hard) blockers.push(`- [收敛未覆盖] ${detail}——状态列自报不作数;命令须是全量门(npm test / verify.mjs)且 workflow/verify-evidence.md 有不早于本 spec 日期的全绿行;路径只认存在(检查 23,含 .vue 与中文目录)。若本 spec 已标 done:先标后补会与本门互为前提(verify 门序含 check-loop)——解锁序:改正文 → 状态改回 approved(勿改 draft,L3 必须 approved/done)→ confirm-doc 重录指纹 → 再标 done。中间态不要提交`);
114
119
  else warnings.push(`- [WARN 收敛未覆盖] ${detail}——状态列自报不作数,证据须存在或为验证命令(检查 23)`);
115
120
  }
116
121
  }
@@ -22,7 +22,7 @@
22
22
  // B 跳过(沿 pairing 门先例);配置行正则语法错误 → fail-closed(防静默空洞)。
23
23
  // 已知边界:commit-time 门禁,CI / pre-push 复跑面无对应全量检查(push 时刻无暂存 diff 一手事实,
24
24
  // 树级反推违背判据原则)——沿增量门禁 pre-commit-only 先例,声明于 spec 风险评估 R2。
25
- // 用法:node check-lane-surface.mjs [--root <dir>] [--branch <名>] [--staged-file <清单文件>]
25
+ // 用法:node check-lane-surface.mjs [--merge] [--root <dir>] [--branch <名>] [--staged-file <清单文件>]
26
26
  // (--root/--branch/--staged-file 为测试注入位;缺省自 git 发现。staged-file 每行一个仓库相对路径)
27
27
  // 退出码:0 放行(含 advisory);1 阻断(stderr 出 [标签] 消息与修法)
28
28
  import fs from 'node:fs';
@@ -52,6 +52,10 @@ const STAGED = stagedArg
52
52
  : gitOut(ROOT, ['diff', '--cached', '--name-only', '--diff-filter=ACMR']).split(/\r?\n/).filter(Boolean);
53
53
 
54
54
  const isExperiment = /^experiment\//.test(BRANCH);
55
+ // 合并进行中(2026-10-15 自源仓 13f67ef7 回灌):pre-merge-commit 显式传 --merge(git merge 自动提交
56
+ // 窗口 MERGE_HEAD 已不可靠,只认显式旗标);冲突解决后的手动 commit 由 MERGE_HEAD 探测兜底(此时
57
+ // 合并仍在进行、MERGE_HEAD 确定存在)。合并提交不引入新首发改动,各笔已在自身提交时刻过门 → 检查 A 跳过。
58
+ const FORCE_MERGE = argv.includes('--merge');
55
59
  // 泳道协议锚(单源 policy.mjs):无 kit.json 的测试夹具回落 v1(无键=双字段协议从未生效,单字段判)
56
60
  const RISK_LEVEL_SINCE = loadKitPolicy(ROOT).riskLevelSince;
57
61
 
@@ -175,7 +179,10 @@ const hits = patterns.length ? STAGED.filter((p) => patterns.some((re) => re.tes
175
179
  // 本次提交触达到的**最低应然档位**:lane-surfaces 命中隐含 L2(沿原口径),hints 命中按登记档取。
176
180
  // 混档取高——与 level-criteria maxLevel 同一口径(就高不就低在提交面同样成立)。
177
181
  const requiredLevel = maxLevel(hits.length ? 'L2' : '', hintLevel);
178
- if (requiredLevel) {
182
+ const merging = FORCE_MERGE || gitOut(ROOT, ['rev-parse', '-q', '--verify', 'MERGE_HEAD']).trim() !== '';
183
+ if (requiredLevel && merging) {
184
+ advisories.push(`[触达面判低](advisory)合并进行中(${FORCE_MERGE ? '--merge 显式标记' : 'MERGE_HEAD 在场'})——检查 A 跳过:合并提交不引入新首发改动,各笔已在自身提交时刻过门`);
185
+ } else if (requiredLevel) {
179
186
  const active = collectActive();
180
187
  const requiredRank = rankOf(requiredLevel) ?? 1;
181
188
  // 入口是否够格:级别 rank ≥ 应然档。**suspect(级别/risk_level 缺失、非法或分歧)一律不够格**——
@@ -31,10 +31,10 @@ const w = (root, rel, content) => fs.writeFileSync(path.join(root, rel), content
31
31
  // eol='\r\n' 时整文件按 CRLF 写(平台边界:frontmatter 读取容忍性)
32
32
  const intent = (name, fm, eol = '\n') => `---${eol}${fm}${eol}---${eol}# INTENT — ${name}${eol}`;
33
33
  const incident = (fm) => `---\n${fm}\n---\n# INCIDENT\n`;
34
- const run = (root, { branch = 'main', staged = [] } = {}) => {
34
+ const run = (root, { branch = 'main', staged = [], args = [] } = {}) => {
35
35
  const listFile = path.join(root, 'staged.txt');
36
36
  fs.writeFileSync(listFile, staged.join('\n') + '\n');
37
- const r = spawnSync(process.execPath, [SCRIPT, '--root', root, '--branch', branch, '--staged-file', listFile], { encoding: 'utf8' });
37
+ const r = spawnSync(process.execPath, [SCRIPT, '--root', root, '--branch', branch, '--staged-file', listFile, ...args], { encoding: 'utf8' });
38
38
  return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
39
39
  };
40
40
 
@@ -77,6 +77,20 @@ const FM_LOW_NEW = '状态: draft\n级别: L1\n日期: 2026-10-01\n模块: ui';
77
77
  const r = run(d, { branch: 'experiment/poc', staged: ['templates/t.md'] });
78
78
  check('S2c experiment 分支检查 A → advisory 放行', r.code === 0 && r.out.includes('advisory·experiment'), r.out);
79
79
  }
80
+ // --- S2d/S2e:合并进行中 → 检查 A 跳过(2026-10-16 自源仓 13f67ef7 回灌)---
81
+ // git merge 自动提交窗口 MERGE_HEAD 已不可靠 → pre-merge-commit 显式传 --merge(fixture 无 MERGE_HEAD,
82
+ // 恰验证 --merge 独生效);对照:同夹具不带 --merge 时命中面+仅活跃 L1 照拦。
83
+ {
84
+ const d = mkfix();
85
+ base(d);
86
+ const r = run(d, { staged: ['templates/_agents/scripts/x.mjs'], args: ['--merge'] });
87
+ check('S2d --merge 显式标记 → 检查 A 跳过且明示来源',
88
+ r.code === 0 && r.out.includes('合并进行中(--merge 显式标记)') && !r.out.includes('[触达面判低] 暂存命中高风险面'),
89
+ r.out);
90
+ const r2 = run(d, { staged: ['templates/_agents/scripts/x.mjs'] });
91
+ check('S2e 无 --merge 同夹具照拦(对照:跳过仅由旗标/MERGE_HEAD 触发)',
92
+ r2.code === 1 && r2.out.includes('[触达面判低]'), r2.out);
93
+ }
80
94
  // --- S3:命中 + 无活跃入口 → advisory 放行(done 不算活跃)---
81
95
  {
82
96
  const d = mkfix();
@@ -221,10 +221,12 @@ const parentShas = (sha) => {
221
221
 
222
222
  // 合并并集对账:cur 行集须恰为各父行集之并(无丢失、无凭空新增)。返回 null = 认可;否则返回原因。
223
223
  // 父提交无此路径(单侧新增)跳过该父;行比对按整行字符串(与 linesOf/前缀判据同粒度)。
224
- const mergeUnionProblem = (sha, rel, curBuf) => {
224
+ // 2026-10-16 自源仓回灌:签名自 sha 改收 parents 数组——--staged 合并态无提交 sha,由调用方传
225
+ // [HEAD, MERGE_HEAD…];历史全扫(传 parentShas(sha))与 --staged 共用本判据(不新开口子)。
226
+ const mergeUnionProblem = (parents, rel, curBuf) => {
225
227
  const curLines = new Set(linesOf(curBuf));
226
228
  const parentLines = new Set();
227
- for (const p of parentShas(sha)) {
229
+ for (const p of parents) {
228
230
  const b = tryBlob(`${p}:${rel}`);
229
231
  if (b === null) continue;
230
232
  for (const l of linesOf(b)) parentLines.add(l);
@@ -309,7 +311,7 @@ function scanHistory(rel, { allowShardDelete }) {
309
311
  if (parents.length >= 2) {
310
312
  // 多父提交无条件走并集对账(前缀通过不得短路——ours-resolution 合并树与第一父逐字节同 blob,
311
313
  // 嵌套判法对静默丢行全盲)
312
- const unionFail = mergeUnionProblem(sha, rel, cur);
314
+ const unionFail = mergeUnionProblem(parentShas(sha), rel, cur);
313
315
  if (!unionFail) {
314
316
  console.error(`[info] ${sha.slice(0, 10)}:合并并集无丢失——union 合并语义认可,续扫`);
315
317
  continue;
@@ -363,6 +365,18 @@ function ledgerPaths() {
363
365
  }
364
366
 
365
367
  // ---- --staged:暂存台账面 ⊇ HEAD 前缀,或携带 manifest 的特许修剪;分片删除须已并入暂存主台账 ----
368
+ // 合并态口径(2026-10-16 自源仓 13f67ef7 回灌 staged-merge-union):MERGE_HEAD 在场时暂存内容是**多父
369
+ // 合并结果**,单看 HEAD 一父不够——合法合并形态下暂存 = 父母并集(− manifest removes),严格前缀等式
370
+ // 必然不等而误拦。此时在严格等格外**再试父母并集集合对账**——与历史全扫同一函数 mergeUnionProblem /
371
+ // mergePruneProblem、同一口径,不新开口子:并集须无丢失、无凭空行(既禁丢行也禁凭空行)。非合并态不启用。
372
+ const mergeParents = (() => {
373
+ const r = spawnSync('git', ['rev-parse', 'MERGE_HEAD'], { cwd: ROOT, encoding: 'utf8' }); // octopus 可多行
374
+ if (r.status !== 0) return null; // 非合并态;spawnSync 吞 stderr,不外泄 fatal 噪声
375
+ const shas = String(r.stdout || '').split(/\s+/).filter(Boolean);
376
+ if (!shas.length) return null;
377
+ const h = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
378
+ return [String(h.stdout || '').trim(), ...shas];
379
+ })();
366
380
  if (staged) {
367
381
  const stagedHit = gitText(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).split(/\r?\n/).filter(Boolean);
368
382
  const touched = ledgerPaths().filter((rel) => stagedHit.includes(rel));
@@ -388,9 +402,21 @@ if (staged) {
388
402
  console.log(`台账不变量(--staged)— ${rel} 特许修剪通过:移除 ${m.removed} 行(${m.docs.length} 家族)——凭据 ${PRUNE_REL} 随笔入库,被删行可在 git 历史对质`);
389
403
  continue;
390
404
  }
405
+ // 合并态(2026-10-16 回灌):严格前缀不适用 → 先纯并集对账,再落 manifest 修剪集合对账
406
+ // (与历史全扫的判定顺序一致)。非合并态两者皆拒。
407
+ const unionFail = mergeParents ? mergeUnionProblem(mergeParents, rel, cur) : '非合并态';
408
+ if (!unionFail) {
409
+ console.log(`台账不变量(--staged)— ${rel} 合并并集无丢失认可(${mergeParents.length} 父)——暂存行集恰为父母行集之并(无丢失、无凭空行),union 语义与历史全扫同口径`);
410
+ continue;
411
+ }
412
+ const setErr = mergeParents ? mergePruneProblem(mergeParents, rel, cur, loadManifestFromRef(`:${PRUNE_REL}`, ROOT)) : '非合并态';
413
+ if (!setErr) {
414
+ console.log(`台账不变量(--staged)— ${rel} 合并携带特许修剪通过:父母并集集合对账认可(${mergeParents.length} 父)——凭据 ${PRUNE_REL} 随笔入库,被删行可在 git 历史对质`);
415
+ continue;
416
+ }
391
417
  let off = 0;
392
418
  while (off < Math.min(head.length, cur.length) && head[off] === cur[off]) off++;
393
- problems.push(`- [台账重写] ${rel} 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${head.length}B → 暂存 ${cur.length}B)${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}`);
419
+ problems.push(`- [台账重写] ${rel} 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${head.length}B → 暂存 ${cur.length}B)${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}${mergeParents ? `;合并并集集合对账亦失败:${setErr}` : ''}${mergeParents ? `;合并纯并集对账失败:${unionFail}` : ''}`);
394
420
  }
395
421
  continue;
396
422
  }
@@ -591,6 +591,34 @@ const mkMergeRepo = () => {
591
591
  fs.rmSync(d, { recursive: true, force: true });
592
592
  }
593
593
 
594
+ // M12(2026-10-16 自源仓 13f67ef7 回灌补夹具):合并态 --staged 删掉两父母共有行(不在 manifest
595
+ // removes 里)→ 仍拦。共享行被删是 union 语义下的真丢失——manifest 只申报了分支侧行,共有行无凭据消失。
596
+ const mkMergePruneRepo = () => {
597
+ const d = mkrepo();
598
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
599
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/gone.md' });
600
+ const comp = row({ ts: '2026-10-13T00:00:00.000Z', stage: 'done', note: '补偿行:doc 仍在' });
601
+ append(d, r2); commitAll(d, 'main r2');
602
+ G(d, ['checkout', '-qb', 'br']);
603
+ fs.writeFileSync(ledPath(d), [row(), comp].join('\n') + '\n');
604
+ writeManifest(d, { ts: '2026-10-13T00:00:00.000Z', authority: 't', reason: 'branch prune', removes: [sha256Of(r2)], docs: ['gone'] });
605
+ commitAll(d, 'br prune + compensation');
606
+ G(d, ['checkout', '-q', BR]);
607
+ fs.writeFileSync(path.join(d, 'other.txt'), 'x');
608
+ commitAll(d, 'main unrelated change');
609
+ G(d, ['merge', '--no-commit', 'br']);
610
+ return { d, r2, comp };
611
+ };
612
+ {
613
+ const { d, comp } = mkMergePruneRepo();
614
+ fs.writeFileSync(ledPath(d), comp + '\n'); // 双父共有行 row() 被删,comp 也不在 removes 内
615
+ G(d, ['add', '-A']);
616
+ const r = run(d, ['--staged']);
617
+ check('M12 合并态 --staged 删双父共有行 → exit 1',
618
+ r.code === 1 && /合并仍丢失 manifest 外/.test(r.out), r.out);
619
+ fs.rmSync(d, { recursive: true, force: true });
620
+ }
621
+
594
622
  // ---- 历史基线(2026-10-13 ledger-history-baseline):owner 声明豁免前基线时代 ----
595
623
  // B1:前基线改写无凭据——无基线全史扫描拦;LEDGER_HISTORY_BASELINE 指到 HEAD 后豁免过
596
624
  // (kit 缺省无基线 = 全量扫描,与源实现「缺省基线指向自身仓提交、外来仓退化为全扫」同语义)
@@ -35,14 +35,6 @@ const check = (name, cond, detail = '') => {
35
35
  else { fail++; console.log(`FAIL ${name}${detail ? `\n ${String(detail).slice(0, 800)}` : ''}`); }
36
36
  };
37
37
 
38
- function shBin() {
39
- for (const bin of ['sh', 'bash']) {
40
- const r = spawnSync(bin, ['-c', 'echo ok'], { encoding: 'utf8' });
41
- if (!r.error && r.status === 0) return bin;
42
- }
43
- return null;
44
- }
45
-
46
38
  function gitRun(cwd, args) {
47
39
  const r = spawnSync(git, args, { cwd, encoding: 'utf8' });
48
40
  if (r.status !== 0) throw new Error(`${args.join(' ')}\n${r.stderr || r.stdout}`);
@@ -145,17 +137,19 @@ try {
145
137
  console.log('SKIP doctor 契约核对(装户位置无包源 src/)');
146
138
  }
147
139
 
148
- const sh = shBin();
149
- check('找到 sh 以跑 pre-push', !!sh);
150
- if (sh && !HOOK) console.log('SKIP pre-push 相关场景(未找到 _githooks / .githooks 下的 pre-push)');
151
- if (sh && HOOK) {
140
+ // pre-push 已 node 化(2026-10-16 自源仓回灌):直调 node 跑钩子,桩改 check-loop.mjs / commit-check.cjs
141
+ if (!HOOK) console.log('SKIP pre-push 相关场景(未找到 _githooks / .githooks 下的 pre-push)');
142
+ if (HOOK) {
152
143
  fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
144
+ fs.mkdirSync(path.join(root, '.agents', 'hooks'), { recursive: true });
153
145
  const node = process.execPath.replace(/\\/g, '/');
154
146
  const loopSh = LOOP.replace(/\\/g, '/');
155
- fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), `#!/bin/sh\nexec "${node}" "${loopSh}" "$@"\n`);
147
+ fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.mjs'), `import { spawnSync } from 'node:child_process';\nconst r = spawnSync(${JSON.stringify(node)}, [${JSON.stringify(loopSh)}, ...process.argv.slice(2)], { stdio: 'inherit' });\nprocess.exit(r.status ?? 1);\n`);
148
+ // 构建门桩:零退出占位——本套件测 --rev 扫描面,不测构建触发
149
+ fs.writeFileSync(path.join(root, '.agents', 'hooks', 'commit-check.cjs'), 'process.exit(0);\n');
156
150
  const hookPath = path.join(root, 'pre-push');
157
151
  fs.copyFileSync(HOOK, hookPath);
158
- const push = (input) => spawnSync(sh, [hookPath], { cwd: root, input, encoding: 'utf8', timeout: 120000 });
152
+ const push = (input) => spawnSync(process.execPath, [hookPath], { cwd: root, input, encoding: 'utf8', timeout: 120000 });
159
153
  const onlyDelete = push(`refs/heads/old ${Z} refs/heads/old ${'b'.repeat(40)}\n`);
160
154
  check('stdin 只有删除行:不阻断', onlyDelete.status === 0, outOf(onlyDelete));
161
155
  const mixed = push(
@@ -92,6 +92,8 @@
92
92
  // quote 字段职责冲突(对质凭据 vs 并录指纹,现归还单一职责)。无 batch 的历史行静默跳过
93
93
  // (无判定依据的行不产出不可消除噪声——沿 audit-gate-hardening P3 教训)
94
94
  // 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block]
95
+ // (2026-10-16 快径:当前树 package.json 无 version 字段 → 整段跳过——无 version 则版本变化提交
96
+ // 永不存在恒不触发,省全历史 git 扫描;源仓「registry 登记恒不触发」同款口径)
95
97
  // 18. 委派台账对账 [warning](policy v4 加 delegationSince 生效日豁免:日期早于锚的 L2/L3 存量豁免——
96
98
  // delegations.md 记法自始不含文件名、无豁免时 113 条不可消退;缺键 v1-v3 维持原全量对账)
97
99
  // 19. 逐阶段审计(起草先于入口确认 / 台账审批顺序倒置) [warning]
@@ -1422,7 +1424,13 @@ export function runCheckLoop(opts = {}) {
1422
1424
  // policy.check17UnclosedAfter 时阻断。版本 1 没有该键,因此不启用后两格。
1423
1425
  // 版本按主、次、修订三段整数比较。沿 package.json 的全部历史查找,不设次数上限。
1424
1426
  // 该提交之后新建的文件不在范围内。open 的 incident 不在此列。
1427
+ // 快径(2026-10-16 减重 自源仓回灌):当前树 package.json 无 version 字段(装户非 node 仓 / 快照树)
1428
+ // → 整段跳过——无 version 则「version 变化的提交」永不存在,本段恒不触发,省去全历史 git log+show
1429
+ // 扫描。适配点:源仓 s17 为独立模块函数用 return 快返;kit 单体函数内改 if 包裹,保后续段照常执行。
1425
1430
  {
1431
+ let curVer = null;
1432
+ try { curVer = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version; } catch { curVer = null; }
1433
+ if (curVer) {
1426
1434
  const log = gitOut(['log', '--format=%H', '--', 'package.json']);
1427
1435
  if (log) {
1428
1436
  let release = null;
@@ -1452,6 +1460,7 @@ export function runCheckLoop(opts = {}) {
1452
1460
  }
1453
1461
  }
1454
1462
  }
1463
+ }
1455
1464
  }
1456
1465
  } // laneOn(17) 守卫闭合
1457
1466
 
@@ -3765,21 +3765,33 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
3765
3765
  check('检查25 预判 L2 落位 L1 → WARN「预判级别不符」含判低信号',
3766
3766
  r.status === 0 && outOf(r).includes('预判级别不符') && outOf(r).includes('判低信号'), `exit=${r.status}\n${outOf(r)}`);
3767
3767
  rmfix(T3);
3768
- // ④ 词表非法 kind / 整字段不可解析 → WARN「预判字段不可解析」
3768
+ // ④ 词表非法 kind / 整字段不可解析 → WARN「预判字段不可解析」(夹具取 approved 活文档——终态 done
3769
+ // 自 2026-10-15 起形态类告警跳过,见 ⑥)
3769
3770
  const T4 = mkfix();
3770
- w(T4, 'workflow/intents/2026-09-12-pj5.md', INTENT('pj5', '状态: done\n级别: L1\n日期: 2026-09-12\n预判: kind=bug level=L1', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
3771
- w(T4, 'workflow/plans/2026-09-12-pj5.md', PLAN('pj5', '状态: done\n级别: L1'));
3771
+ w(T4, 'workflow/intents/2026-09-12-pj5.md', INTENT('pj5', '状态: approved\n级别: L1\n日期: 2026-09-12\n预判: kind=bug level=L1', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
3772
+ w(T4, 'workflow/plans/2026-09-12-pj5.md', PLAN('pj5', '状态: draft\n级别: L1'));
3772
3773
  r = run(T4);
3773
3774
  check('检查25 kind=bug 不在词表 → WARN「预判字段不可解析」',
3774
3775
  r.status === 0 && outOf(r).includes('预判字段不可解析'), `exit=${r.status}\n${outOf(r)}`);
3775
3776
  rmfix(T4);
3776
3777
  const T5 = mkfix();
3777
- w(T5, 'workflow/intents/2026-09-12-pj6.md', INTENT('pj6', '状态: done\n级别: L1\n日期: 2026-09-12\n预判: 这是个修复', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
3778
- w(T5, 'workflow/plans/2026-09-12-pj6.md', PLAN('pj6', '状态: done\n级别: L1'));
3778
+ w(T5, 'workflow/intents/2026-09-12-pj6.md', INTENT('pj6', '状态: approved\n级别: L1\n日期: 2026-09-12\n预判: 这是个修复', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
3779
+ w(T5, 'workflow/plans/2026-09-12-pj6.md', PLAN('pj6', '状态: draft\n级别: L1'));
3779
3780
  r = run(T5);
3780
3781
  check('检查25 预判字段无 kv 形态 → WARN「预判字段不可解析」',
3781
3782
  r.status === 0 && outOf(r).includes('预判字段不可解析'), `exit=${r.status}\n${outOf(r)}`);
3782
3783
  rmfix(T5);
3784
+ // ⑥ 终态跳过(2026-10-15):done 文档词表/形态类告警不产——终态受检查 15 内容绑定不可修订,
3785
+ // WARN 永不可消(「不可消除噪声」红线);类型/级别比对照常(level 一致;类型告警不在本断言面)
3786
+ {
3787
+ const T6 = mkfix();
3788
+ w(T6, 'workflow/intents/2026-09-12-pj7.md', INTENT('pj7', '状态: done\n级别: L1\n日期: 2026-09-12\n预判: kind=bug level=L1', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
3789
+ w(T6, 'workflow/plans/2026-09-12-pj7.md', PLAN('pj7', '状态: done\n级别: L1'));
3790
+ r = run(T6);
3791
+ check('检查25 终态 done 词表外 kind → 形态类告警不产(2026-10-15 终态跳过)',
3792
+ r.status === 0 && !outOf(r).includes('预判字段不可解析'), `exit=${r.status}\n${outOf(r)}`);
3793
+ rmfix(T6);
3794
+ }
3783
3795
  }
3784
3796
 
3785
3797
  // ---- 检查 26:闭环漏斗「协议前」占比动作阈值(相邻两月均 >30% → WARN;消费 gen-workflow-metrics 产物)----
@@ -29,16 +29,21 @@ export function run(ctx) {
29
29
  const pj = fmGet(abs, '预判');
30
30
  if (!pj) continue;
31
31
  const rel = `${WF}/${sub}/${f}`;
32
+ // 终态文档(2026-10-15):形态/词表类告警跳过——终态受检查 15 内容绑定不可修订,此类 WARN 永不可
33
+ // 消(「不可消除噪声」红线);类型/级别比对保留——那是路由审计的本体信号,灰度对账照常采集。
34
+ // (终态并集 = doc 终态 done/superseded/cancelled + incident 终态 fixed/closed——两侧枚举无单一键
35
+ // 覆盖该并集,故与源仓 s25 同款内联声明。)
36
+ const terminalDoc = ['done', 'superseded', 'cancelled', 'fixed', 'closed'].includes(fmGet(abs, '状态'));
32
37
  const kv = {};
33
38
  for (const m of pj.matchAll(/(kind|level)=([^\s]+)/g)) kv[m[1]] = m[2];
34
39
  if (!Object.keys(kv).length) {
35
- warnings.push(`- [WARN 预判字段不可解析] 须形如「预判: kind=<kind> level=<L1|L2|L3>」:${rel}`);
40
+ if (!terminalDoc) warnings.push(`- [WARN 预判字段不可解析] 须形如「预判: kind=<kind> level=<L1|L2|L3>」:${rel}`);
36
41
  continue;
37
42
  }
38
- if (kv.kind && !KINDS.includes(kv.kind)) {
43
+ if (kv.kind && !KINDS.includes(kv.kind) && !terminalDoc) {
39
44
  warnings.push(`- [WARN 预判字段不可解析] kind=${kv.kind} 不在词表(${KINDS.join('/')}):${rel}`);
40
45
  }
41
- if (kv.level && !inSet(kv.level, ENUMS['level.all'])) {
46
+ if (kv.level && !inSet(kv.level, ENUMS['level.all']) && !terminalDoc) {
42
47
  warnings.push(`- [WARN 预判字段不可解析] level=${kv.level} 不在枚举 level.all:${rel}`);
43
48
  }
44
49
  // 类型比对:intents ≡ require、incidents ≡ fix(plans/specs 两道共用,不判类型)