agentic-flow-kit 1.6.0 → 1.7.15

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 (115) hide show
  1. package/README.md +40 -6
  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 +2 -2
  10. package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
  11. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  12. package/modules/hosts/claude/commands/wf-source-sync-check.md +1 -3
  13. package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -3
  14. package/modules/hosts/claude/commands/wf-test.md +5 -4
  15. package/modules/hosts/codex/commands/wf-build.md +5 -24
  16. package/modules/hosts/codex/commands/wf-deploy.md +1 -0
  17. package/modules/hosts/codex/commands/wf-design.md +2 -2
  18. package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
  19. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  20. package/modules/hosts/codex/commands/wf-source-sync-check.md +1 -3
  21. package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -3
  22. package/modules/hosts/codex/commands/wf-test.md +5 -4
  23. package/modules/hosts/cursor/commands/wf-build.md +5 -24
  24. package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
  25. package/modules/hosts/cursor/commands/wf-design.md +2 -2
  26. package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
  27. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  28. package/modules/hosts/cursor/commands/wf-source-sync-check.md +1 -3
  29. package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -3
  30. package/modules/hosts/cursor/commands/wf-test.md +5 -4
  31. package/modules/hosts/opencode/commands/wf-build.md +5 -24
  32. package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
  33. package/modules/hosts/opencode/commands/wf-design.md +2 -2
  34. package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
  35. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  36. package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -3
  37. package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -3
  38. package/modules/hosts/opencode/commands/wf-test.md +5 -4
  39. package/modules/hosts/trae/commands/wf-build.md +5 -24
  40. package/modules/hosts/trae/commands/wf-deploy.md +1 -0
  41. package/modules/hosts/trae/commands/wf-design.md +2 -2
  42. package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
  43. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  44. package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -3
  45. package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -3
  46. package/modules/hosts/trae/commands/wf-test.md +5 -4
  47. package/package.json +2 -2
  48. package/src/add-gate.mjs +25 -1
  49. package/src/add-gate.test.mjs +103 -0
  50. package/src/cli.mjs +17 -2
  51. package/src/doctor.mjs +99 -11
  52. package/src/gates.test.mjs +17 -0
  53. package/src/init.mjs +7 -1
  54. package/src/init.test.mjs +19 -0
  55. package/src/instance-ref.test.mjs +62 -0
  56. package/src/jscpd-drift.test.mjs +144 -0
  57. package/src/render.mjs +5 -0
  58. package/src/rule-budget.test.mjs +103 -0
  59. package/src/run-tests.mjs +16 -9
  60. package/src/sync-hosts.mjs +4 -2
  61. package/src/trae-hooks.test.mjs +15 -3
  62. package/src/validate.mjs +72 -0
  63. package/src/validate.test.mjs +95 -0
  64. package/templates/AGENTS.md +3 -1
  65. package/templates/_agents/commands/build.md +5 -24
  66. package/templates/_agents/commands/deploy.md +1 -0
  67. package/templates/_agents/commands/design.md +2 -2
  68. package/templates/_agents/commands/gate-checklist.md +1 -3
  69. package/templates/_agents/commands/plan.md +1 -1
  70. package/templates/_agents/commands/source-sync-check.md +1 -3
  71. package/templates/_agents/commands/sync-hosts.md +1 -3
  72. package/templates/_agents/commands/test.md +5 -4
  73. package/templates/_agents/control-surfaces.txt +53 -0
  74. package/templates/_agents/scripts/agg-delegations.cjs +51 -6
  75. package/templates/_agents/scripts/agg-delegations.test.mjs +79 -1
  76. package/templates/_agents/scripts/check-engine-integrity.mjs +288 -0
  77. package/templates/_agents/scripts/check-engine-integrity.test.mjs +227 -0
  78. package/templates/_agents/scripts/check-lane-surface.mjs +7 -2
  79. package/templates/_agents/scripts/check-ledger-invariant.mjs +18 -0
  80. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +16 -0
  81. package/templates/_agents/scripts/check-ledger.test.mjs +2 -0
  82. package/templates/_agents/scripts/check-loop.mjs +247 -5
  83. package/templates/_agents/scripts/check-loop.test.mjs +367 -2
  84. package/templates/_agents/scripts/confirm-doc.mjs +50 -3
  85. package/templates/_agents/scripts/confirm-doc.test.mjs +93 -1
  86. package/templates/_agents/scripts/doctor.test.mjs +251 -1
  87. package/templates/_agents/scripts/draft-sediment.mjs +178 -0
  88. package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
  89. package/templates/_agents/scripts/gate-checklist.mjs +10 -2
  90. package/templates/_agents/scripts/gate-checklist.test.mjs +7 -1
  91. package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
  92. package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
  93. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +2 -2
  94. package/templates/_agents/scripts/gen-workflow-metrics.mjs +11 -5
  95. package/templates/_agents/scripts/kb-search.mjs +87 -12
  96. package/templates/_agents/scripts/kb-search.test.mjs +83 -0
  97. package/templates/_agents/scripts/pipeline-run.mjs +1 -1
  98. package/templates/_agents/scripts/policy.mjs +49 -0
  99. package/templates/_agents/scripts/solidify-task.mjs +1 -1
  100. package/templates/_agents/scripts/source-sync-check.test.mjs +9 -4
  101. package/templates/_agents/scripts/stage-gates.mjs +48 -1
  102. package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
  103. package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
  104. package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
  105. package/templates/_agents/scripts/verify.mjs +12 -4
  106. package/templates/_agents/scripts/verify.test.mjs +30 -3
  107. package/templates/_agents/scripts-test-exempt.txt +3 -1
  108. package/templates/_agents/skills/wiki/SKILL.md +5 -3
  109. package/templates/wiki/INDEX.md +44 -1
  110. package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
  111. package/templates/workflow/delegations.md +5 -4
  112. package/templates/workflow/incidents/_TEMPLATE.md +2 -0
  113. package/templates/workflow/intents/_TEMPLATE.md +3 -0
  114. package/templates/workflow/plans/_TEMPLATE.md +4 -0
  115. package/templates/workflow/specs/_TEMPLATE.md +23 -0
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ // check-engine-integrity.test.mjs — 门禁本体完整性门 fixture 套件(2026-10-09 P0-A guard-the-guards)
3
+ // 方法:构造临时根(templates/_agents/scripts + .githooks + workflow/intents),spawn 脚本断言
4
+ // exit code 与 [标签];跨提交锚用真 git 仓(init→commit→改弱→--update)端到端验证——
5
+ // 那一条是本门存在的理由:本地层自洽(盘面↔基线同刷)时,唯祖先提交参照物仍能出账。
6
+ // 用法:node templates/_agents/scripts/check-engine-integrity.test.mjs(npm test 随跑)
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const SCRIPT = path.join(SCRIPT_DIR, 'check-engine-integrity.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 w = (root, rel, content) => {
24
+ const abs = path.join(root, rel);
25
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
26
+ fs.writeFileSync(abs, content);
27
+ };
28
+ const run = (root, args = []) => {
29
+ const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
30
+ return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
31
+ };
32
+ const git = (root, args) => spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
33
+ const haveGit = (() => { const r = spawnSync('git', ['--version'], { encoding: 'utf8' }); return r.status === 0; })();
34
+
35
+ // 通用 fixture:一份门禁脚本(2 个 blocker + 1 个 exit(1) = 3 硬拦单位)、一个钩子(1 个 exit 1)、AGENTS.md
36
+ const mkfix = ({ withEntry = false, entryNames = 'gate.mjs' } = {}) => {
37
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-integrity-test-'));
38
+ w(d, 'templates/_agents/scripts/gate.mjs',
39
+ '// 门禁样例\nblockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n');
40
+ w(d, '.githooks/pre-commit', '#!/bin/sh\nif ! node check.mjs; then\n echo blocked >&2\n exit 1\nfi\nexit 0\n');
41
+ w(d, 'AGENTS.md', '# 协议\n- 规则一\n- 规则二\n');
42
+ if (withEntry) {
43
+ w(d, 'workflow/intents/2026-10-09-ctrl.md',
44
+ `---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:templates/_agents/scripts/${entryNames}\n`);
45
+ }
46
+ return d;
47
+ };
48
+
49
+ // --- T1 基线缺失 = 未启用(旧装副本零改动,exit 0)---
50
+ {
51
+ const r = run(mkfix());
52
+ check('T1 无基线 → exit 0 且报「未启用」', r.code === 0 && r.out.includes('未启用'), r.out);
53
+ }
54
+
55
+ // --- T2 --update 生成基线后自身一致 ---
56
+ {
57
+ const d = mkfix();
58
+ const u = run(d, ['--update']);
59
+ // fixture 无 .agents/ → 基线落包源径(resolveLock 布局回落,见 T9/T10)
60
+ const lockPath = path.join(d, 'templates', '_agents', 'engine-lock.json');
61
+ let lock = null;
62
+ try { lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch { /* 断言兜 */ }
63
+ const r = run(d);
64
+ check('T2a --update → exit 0 且基线入账受检面', u.code === 0 && !!lock && !!lock.files['templates/_agents/scripts/gate.mjs'], u.out);
65
+ check('T2b 基线记 hard=3(2 blocker + 1 exit(1))', !!lock && lock.files['templates/_agents/scripts/gate.mjs'].hard === 3, JSON.stringify(lock && lock.files['templates/_agents/scripts/gate.mjs']));
66
+ check('T2c 钩子入面 hard=1(exit 1 计数)', !!lock && lock.files['.githooks/pre-commit'].hard === 1, JSON.stringify(lock && lock.files['.githooks/pre-commit']));
67
+ check('T2d 盘面未变 → exit 0 且报「与基线一致」', r.code === 0 && r.out.includes('盘面与基线一致'), r.out);
68
+ }
69
+
70
+ // --- T3 硬拦单位数下降 → 无条件拦(有入口也不放行)---
71
+ {
72
+ const d = mkfix({ withEntry: true });
73
+ run(d, ['--update']);
74
+ w(d, 'templates/_agents/scripts/gate.mjs', '// 门禁样例(退役一条 blocker)\nblockers.push(`A`);\nif (bad) process.exit(1);\n');
75
+ const r = run(d);
76
+ check('T3 删 blocker.push → [硬拦面收缩] 且点名文件与减量',
77
+ r.code === 1 && r.out.includes('[硬拦面收缩]') && r.out.includes('gate.mjs') && r.out.includes('3 → 2'), r.out);
78
+ check('T3b 有活跃 L2 入口仍拦(收缩无豁免出口)', r.code === 1 && r.out.includes('[硬拦面收缩]'), r.out);
79
+ }
80
+
81
+ // --- T4 非收缩改动:无入口拦、有入口放行 ---
82
+ {
83
+ const d = mkfix();
84
+ run(d, ['--update']);
85
+ w(d, 'templates/_agents/scripts/gate.mjs', '// 门禁样例(改写文案)\n// 说明行\ntoast(`hint`);\nblockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n');
86
+ const noEntry = run(d);
87
+ check('T4a 内容漂移 + 无入口 → [控制面改动无入口]',
88
+ noEntry.code === 1 && noEntry.out.includes('[控制面改动无入口]') && noEntry.out.includes('gate.mjs'), noEntry.out);
89
+ w(d, 'workflow/intents/2026-10-09-ctrl.md',
90
+ '---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:templates/_agents/scripts/gate.mjs\n');
91
+ const withEntry = run(d);
92
+ check('T4b 同一改动 + 活跃 L2 入口点名该件 → 放行', withEntry.code === 0, withEntry.out);
93
+ }
94
+ // --- T4c 入口存在但级别为 L1(协作道)→ 不算点名 ---
95
+ {
96
+ const d = mkfix({ withEntry: false });
97
+ run(d, ['--update']);
98
+ w(d, 'templates/_agents/scripts/gate.mjs', 'blockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n// 追加注释不改硬拦面\n');
99
+ w(d, 'workflow/intents/2026-10-09-low.md', '---\n状态: approved\n级别: L1\n日期: 2026-10-09\n模块: gate\n---\n改动面:gate.mjs\n');
100
+ const r = run(d);
101
+ check('T4c L1 入口点名不豁免(控制面须防御道)', r.code === 1 && r.out.includes('[控制面改动无入口]'), r.out);
102
+ }
103
+ // --- T4d 入口非活跃(done 已关单)→ 不算点名 ---
104
+ {
105
+ const d = mkfix({ withEntry: false });
106
+ run(d, ['--update']);
107
+ w(d, 'templates/_agents/scripts/gate.mjs', 'blockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n// 追加注释\n');
108
+ w(d, 'workflow/intents/2026-10-09-done.md', '---\n状态: done\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n改动面:gate.mjs\n');
109
+ const r = run(d);
110
+ check('T4d done 入口不点名(活跃口径:draft/approved)', r.code === 1 && r.out.includes('[控制面改动无入口]'), r.out);
111
+ }
112
+
113
+ // --- T5 门禁件消失 / 新增件未入基线 ---
114
+ {
115
+ const d = mkfix();
116
+ run(d, ['--update']);
117
+ fs.rmSync(path.join(d, 'templates/_agents/scripts/gate.mjs'));
118
+ let r = run(d);
119
+ check('T5a 删除门禁件 → [门禁件消失]', r.code === 1 && r.out.includes('[门禁件消失]'), r.out);
120
+ }
121
+ {
122
+ const d = mkfix();
123
+ run(d, ['--update']);
124
+ w(d, 'templates/_agents/scripts/newgate.mjs', 'blockers.push(`X`);\n');
125
+ const r = run(d);
126
+ check('T5b 新受检件未入账 → [门禁件未入基线],--update 后放行',
127
+ r.code === 1 && r.out.includes('[门禁件未入基线]') && r.out.includes('newgate.mjs'), r.out);
128
+ run(d, ['--update']);
129
+ check('T5c --update 把新件入账后 exit 0', run(d).code === 0);
130
+ }
131
+
132
+ // --- T6 外部副本锚 ---
133
+ {
134
+ const d = mkfix();
135
+ run(d, ['--update']);
136
+ const bad = run(d, ['--against', path.join(d, 'nope')]);
137
+ check('T6a --against 目录不存在 → 用法错误 exit 2', bad.code === 2 && bad.out.includes('目录不存在'), bad.out);
138
+ const ext = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-integrity-ext-'));
139
+ w(ext, 'templates/_agents/scripts/gate.mjs', '// 外部已知良好副本(与盘面不同)\nblockers.push(`A`);\n');
140
+ const r = run(d, ['--against', ext]);
141
+ check('T6b 与外部副本逐字失配 → [外部锚失配]', r.code === 1 && r.out.includes('[外部锚失配]'), r.out);
142
+ }
143
+
144
+ // --- T7 跨提交锚(真 git 仓端到端:本门存在的理由)---
145
+ if (haveGit) {
146
+ const d = mkfix();
147
+ git(d, ['init', '-q']);
148
+ git(d, ['config', 'user.email', 't@example.com']);
149
+ git(d, ['config', 'user.name', 'fixture']);
150
+ run(d, ['--update']);
151
+ git(d, ['add', '-A']);
152
+ git(d, ['commit', '-q', '-m', 'chore: 基线入账']);
153
+ // 改弱门禁 + 同批刷基线:本地层完全自洽(这正是 CI 自指的形态)
154
+ w(d, 'templates/_agents/scripts/gate.mjs', '// 退役一条判据\nblockers.push(`A`);\nif (bad) process.exit(1);\n');
155
+ run(d, ['--update']);
156
+ const local = run(d);
157
+ check('T7a 改弱 + 刷基线 → 本地层自洽放行(缺陷场景复现)', local.code === 0, local.out);
158
+ const anchored = run(d, ['--against-ref', 'HEAD']);
159
+ check('T7b 跨提交锚(祖先提交基线)出账 [硬拦面收缩(跨提交锚)]',
160
+ anchored.code === 1 && anchored.out.includes('[硬拦面收缩(跨提交锚)]') && /3(见 HEAD)→ 盘面 2/.test(anchored.out), anchored.out);
161
+ // 合法退役出口:活跃 L2 入口点名该件
162
+ w(d, 'workflow/intents/2026-10-09-retire.md',
163
+ '---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:templates/_agents/scripts/gate.mjs(退役 B 判据)\n');
164
+ git(d, ['add', '-A']);
165
+ git(d, ['commit', '-q', '-m', 'docs(gate): L2 入口留痕']);
166
+ const declared = run(d, ['--against-ref', 'HEAD~1']);
167
+ check('T7c 声明过的收缩(L2 入口点名)→ 跨提交锚放行', declared.code === 0, declared.out);
168
+ // 锚缺失(首个入账提交之前)→ 跳过不误报
169
+ const missing = run(d, ['--against-ref', 'HEAD~9']);
170
+ check('T7d ref 不可解析 → 锚缺失跳过(exit 0 不假阳性)', missing.code === 0 && missing.out.includes('跨提交锚缺失'), missing.out);
171
+ // 严格档反例(T7e):正文顺手提及 ≠ 声明改动面——存量无关 L2 文档不得替改弱行为背书
172
+ w(d, 'workflow/intents/2026-10-09-retire.md',
173
+ '---\n状态: superseded\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:templates/_agents/scripts/gate.mjs\n');
174
+ w(d, 'workflow/intents/2026-10-09-prose.md',
175
+ '---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n本批沿用 gate.mjs 与 check-loop.mjs 的既有判据,未改动它们。\n');
176
+ git(d, ['add', '-A']);
177
+ git(d, ['commit', '-q', '-m', 'docs(gate): prose 提及(非声明改动面)']);
178
+ const prose = run(d, ['--against-ref', 'HEAD~2']);
179
+ check('T7e 正文提及但无 §改动面 声明 → 跨提交锚仍拦(严格档:模糊提及不豁免收缩)',
180
+ prose.code === 1 && prose.out.includes('[硬拦面收缩(跨提交锚)]'), prose.out);
181
+ } else {
182
+ console.log('SKIP T7 跨提交锚(未找到 git)');
183
+ }
184
+
185
+ // --- T8 --quiet 静默面(钩子里调用不刷屏)---
186
+ {
187
+ const d = mkfix();
188
+ run(d, ['--update']);
189
+ const r = run(d, ['--quiet']);
190
+ check('T8 --quiet 通过时不打印 ✅ 行', r.code === 0 && !r.out.includes('✅'), r.out);
191
+ }
192
+
193
+ // --- T9 装户布局:.agents/ 在 → 基线落装户径,不新建 templates/_agents/engine-lock.json ---
194
+ {
195
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-integrity-test-'));
196
+ w(d, '.agents/scripts/gate.mjs', 'blockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n');
197
+ w(d, 'AGENTS.md', '# 协议\n- 规则一\n');
198
+ const u = run(d, ['--update']);
199
+ let lock = null;
200
+ try { lock = JSON.parse(fs.readFileSync(path.join(d, '.agents', 'engine-lock.json'), 'utf8')); } catch { /* 断言兜 */ }
201
+ check('T9a .agents/ 在 → --update 落 .agents/engine-lock.json(装户径行为不变)',
202
+ u.code === 0 && !!lock && !!lock.files['.agents/scripts/gate.mjs'], u.out);
203
+ check('T9b 装户布局不新建 templates/_agents/engine-lock.json',
204
+ !fs.existsSync(path.join(d, 'templates', '_agents', 'engine-lock.json')), '');
205
+ fs.rmSync(d, { recursive: true, force: true });
206
+ }
207
+
208
+ // --- T10 包源布局:无 .agents/ → 基线落包源径,不得重建装户目录(装户端内容清理后回归锁)---
209
+ {
210
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-integrity-test-'));
211
+ w(d, 'templates/_agents/scripts/gate.mjs', 'blockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n');
212
+ w(d, 'templates/AGENTS.md', '# 协议\n- 规则一\n');
213
+ const u = run(d, ['--update']);
214
+ let lock = null;
215
+ try { lock = JSON.parse(fs.readFileSync(path.join(d, 'templates', '_agents', 'engine-lock.json'), 'utf8')); } catch { /* 断言兜 */ }
216
+ check('T10a 无 .agents/ → --update 落 templates/_agents/engine-lock.json 且入账受检面',
217
+ u.code === 0 && !!lock && !!lock.files['templates/_agents/scripts/gate.mjs'], u.out);
218
+ check('T10b --update 不重建 .agents/ 目录(旧实现按装户径兜底会重建已清理的装户目录)',
219
+ !fs.existsSync(path.join(d, '.agents')), '');
220
+ const r = run(d);
221
+ check('T10c 基线启用 → 「盘面与基线一致」且未启用提示点包源径',
222
+ r.code === 0 && r.out.includes('盘面与基线一致'), r.out);
223
+ fs.rmSync(d, { recursive: true, force: true });
224
+ }
225
+
226
+ console.log(`\ncheck-engine-integrity: ${pass} PASS / ${fail} FAIL`);
227
+ process.exit(fail ? 1 : 0);
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // check-lane-surface.mjs — 泳道完整性门禁(pre-commit 增量;2026-10-01 drift-hardening)
3
+ // 布局前提:本门读 .agents/lane-surfaces.txt,豁免出口是 workflow/intents 的活跃 L2/L3 入口——
4
+ // 二者都是**装户侧**件。纯包源仓(装户端内容清理后)两处皆无:清单缺失 = 检查 A/C 按既定
5
+ // 「装户无感」口径跳过,本门整体惰性。此时把清单解析改双路径**只会让门跑起来而不会可用**——
6
+ // 无 L2 入口机器时,任何控制面改动都走「无活跃入口」分支(A 出 advisory / C 出 BLOCK),
7
+ // 纯包源仓的控制面保护改由 npm test(47 套件覆盖门禁逻辑)+ 评审 + CHANGELOG 承担。
3
8
  // 检查 A「触达面判低」:暂存 diff 命中项目 L2 触达面(.agents/lane-surfaces.txt,ERE 逐行;
4
9
  // # 注释与空行忽略;文件缺失或清单为空 = 检查跳过,装户无感)时——
5
10
  // 存在活跃 L2/L3 入口 → 放行(归因歧义为已知边界:机器不猜「哪个入口在干这批改动」);
@@ -10,7 +15,7 @@
10
15
  // 检查 B「探索标记缺失」:experiment/* 分支暂存的 workflow/intents/*.md(排除 _TEMPLATE)
11
16
  // frontmatter 无「阶段: exploring」且无「状态: exploring」(口径同 check-loop 加固门)→ hard 拦。
12
17
  // 标记是加固门覆盖率的前提且补写零成本——advisory 免的是纪律负担,不免标记本身。
13
- // 判据原则(incidents/2026-09-28-batch-ledger-audit 教训):只用一手事实——暂存文件清单、
18
+ // 判据原则(2026-09-28 台账批量代录事故教训):只用一手事实——暂存文件清单、
14
19
  // 当前分支名、工作区 frontmatter 字段;不用时间戳 / 字符串相等反推过程事实。
15
20
  // 边界:git 调用一律 -c core.quotepath=off(非 ASCII 路径教训,同 check-pairing-incremental.sh);
16
21
  // frontmatter 读取容忍 CRLF(显式按 \r?\n 切分);detached HEAD 分支不可判 → A 按 strict、
@@ -171,7 +176,7 @@ if (isExperiment) {
171
176
  for (const a of advisories) console.error(`泳道完整性(增量)— advisory:\n${a}\n`);
172
177
  if (blocks.length) {
173
178
  for (const b of blocks) console.error(`泳道完整性(增量)— BLOCK:\n${b}\n`);
174
- console.error('pre-commit: 泳道完整性未通过,提交已阻止(spec: workflow/specs/2026-10-01-drift-hardening.md)');
179
+ console.error('pre-commit: 泳道完整性未通过,提交已阻止(判据见 .agents/commands/new-task.md §级别判断)');
175
180
  process.exit(1);
176
181
  }
177
182
  process.exit(0);
@@ -16,6 +16,12 @@
16
16
  // ④ doc 匹配 workflow/(intents|specs|plans|incidents)/[^/]+.md 且当前树存在
17
17
  // ⑤ stage ∈ 合法跳转终态集(approved/done/fixed/closed/superseded/cancelled)
18
18
  //
19
+ // 行级豁免(2026-10-09 ledger-line-exempt,CI 红事故处置):.agents/ledger-line-exempt.json 登记
20
+ // 已定性的污染行({line, doc, reason}——line = 校验序号即非空行序)——行级五项对豁免行整体跳过、
21
+ // 行**保留在台账**(审计面完整),豁免计数照常出账保持可见。定位:事故行无法追加修复(行级扫最终树
22
+ // 全行)且不可就地改(历史前缀不变量)——superseded 重立单之外的唯一出口。**滥用边界**:豁免文件
23
+ // 自身入库、git 历史可对质;逐条须 papercuts/incident 定性引用,无定性登记 = 审计事件。
24
+ //
19
25
  // 静默条件:台账不存在 / 零提交历史(装户、新仓)/ --staged 且暂存未触台账或无 HEAD 版本。
20
26
  // 已知边界:git log --follow 未用——台账自创建无重命名(单一路径取史);克隆深度不足时仅扫可得
21
27
  // 历史(CI 侧 fetch-depth:0 兜全史,深度不足会告警声明边界,fail-open 于深度、fail-closed 于内容)。
@@ -102,9 +108,20 @@ let rows = [];
102
108
  try {
103
109
  rows = fs.readFileSync(FILE, 'utf8').split(/\r?\n/).filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return null; } });
104
110
  } catch { /* 上面 existsSync 已兜 */ }
111
+ const EXEMPT_P = path.join(ROOT, '.agents', 'ledger-line-exempt.json');
112
+ const exemptLines = new Set();
113
+ try {
114
+ if (fs.existsSync(EXEMPT_P)) {
115
+ for (const e of JSON.parse(fs.readFileSync(EXEMPT_P, 'utf8'))) {
116
+ if (e && Number.isInteger(e.line)) exemptLines.add(e.line);
117
+ }
118
+ }
119
+ } catch { /* 豁免表损坏 → 视同无豁免(fail-closed 于校验面) */ }
120
+ let exemptCount = 0;
105
121
  let prevTs = '';
106
122
  rows.forEach((e, idx) => {
107
123
  const at = `第 ${idx + 1} 行`;
124
+ if (exemptLines.has(idx + 1)) { exemptCount++; return; } // 事故行豁免(登记文件入库可对质)
108
125
  if (!e || typeof e !== 'object') { problems.push(`- [行级] ${at}:非 JSON 对象行`); return; }
109
126
  if (typeof e.ts !== 'string' || Number.isNaN(Date.parse(e.ts))) problems.push(`- [行级] ${at}:ts 非 ISO 字符串`);
110
127
  else {
@@ -126,6 +143,7 @@ rows.forEach((e, idx) => {
126
143
  if (!VALID_STAGES.has(e.stage)) problems.push(`- [行级] ${at}:stage「${e.stage || '缺失'}」不在合法跳转终态集`);
127
144
  });
128
145
 
146
+ if (exemptCount) console.log(`ℹ️ 台账豁免行 ${exemptCount} 条(定性见 .agents/ledger-line-exempt.json)`);
129
147
  if (problems.length) {
130
148
  console.error(`台账不可变(历史全扫)— HARD-BLOCK:\n\n${problems.join('\n')}\n`);
131
149
  console.error(' 修法:台账历史行禁删改(append-only 前缀不变量)——误写只能追加补偿行;确需撤销历史提交请 git revert 后重新追加。篡改属审计事件,保留现场联系仓库所有者。');
@@ -157,5 +157,21 @@ const append = (d, line) => fs.appendFileSync(ledPath(d), line + '\n');
157
157
  check('S12 回退注记行(n/a 指纹)→ 历史全扫过', r.code === 0, r.out);
158
158
  }
159
159
 
160
+ // ---- 豁免登记(2026-10-09 ledger-line-exempt,CI 红事故处置)----
161
+ {
162
+ // 坏行(fingerprint 非 hex + doc 不存在)→ 红;登记豁免 → 绿 + 计数可见
163
+ const d = mkrepo(row({ fingerprint: 'probe-fp', doc: 'workflow/intents/ghost.md' }));
164
+ let r = run(d);
165
+ check('豁免前:行级坏行 hard(fingerprint/doc 双违规)', r.code === 1 && r.out.includes('行级'), r.out.slice(0, 200));
166
+ fs.writeFileSync(path.join(d, '.agents', 'ledger-line-exempt.json'), JSON.stringify([{ line: 1, doc: 'workflow/intents/ghost.md', reason: '事故定性' }], null, 2));
167
+ r = run(d);
168
+ check('豁免后:exit 0 且计数可见', r.code === 0 && r.out.includes('台账豁免行 1 条'), r.out.slice(0, 200));
169
+ // 行号错位豁免不生效(line=99 无对应坏行)
170
+ fs.writeFileSync(path.join(d, '.agents', 'ledger-line-exempt.json'), JSON.stringify([{ line: 99, reason: '错位' }]));
171
+ r = run(d);
172
+ check('豁免行号不匹配:仍 hard(精确豁免)', r.code === 1, r.out.slice(0, 150));
173
+ fs.rmSync(d, { recursive: true, force: true });
174
+ }
175
+
160
176
  console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
161
177
  process.exit(fail ? 1 : 0);
@@ -117,6 +117,8 @@ const runCli = (cwd) => spawnSync(process.execPath, [SCRIPT], { encoding: 'utf8'
117
117
  }
118
118
  if (!srcRoot || !fs.existsSync(path.join(srcRoot, 'templates', '_agents'))) {
119
119
  console.log('SKIP S9(装户环境无包源,不计失败)');
120
+ } else if (!fs.existsSync(path.join(srcRoot, '.agents', 'kit.json'))) {
121
+ console.log('SKIP S9(纯包源仓无 .agents/ 装副本——装户端内容清理后形态,不计失败)');
120
122
  } else {
121
123
  const r = ledgerDrift(srcRoot);
122
124
  check('S9 真实仓 baseline → 台账↔盘面全对齐', !r.skipped && r.modified.length === 0 && r.gone.length === 0, JSON.stringify({ modified: r.modified, gone: r.gone }));