agentic-flow-kit 1.7.15 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +50 -8
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-design.md +5 -4
  4. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  5. package/modules/hosts/claude/commands/wf-new-task.md +47 -25
  6. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  7. package/modules/hosts/claude/commands/wf-review.md +2 -2
  8. package/modules/hosts/claude/commands/wf-sync-hosts.md +7 -13
  9. package/modules/hosts/claude/commands/wf-test.md +2 -2
  10. package/modules/hosts/codex/commands/wf-build.md +1 -1
  11. package/modules/hosts/codex/commands/wf-design.md +5 -4
  12. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  13. package/modules/hosts/codex/commands/wf-new-task.md +47 -25
  14. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  15. package/modules/hosts/codex/commands/wf-review.md +2 -2
  16. package/modules/hosts/codex/commands/wf-sync-hosts.md +7 -13
  17. package/modules/hosts/codex/commands/wf-test.md +2 -2
  18. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  19. package/modules/hosts/cursor/commands/wf-design.md +5 -4
  20. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  21. package/modules/hosts/cursor/commands/wf-new-task.md +47 -25
  22. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  23. package/modules/hosts/cursor/commands/wf-review.md +2 -2
  24. package/modules/hosts/cursor/commands/wf-sync-hosts.md +7 -13
  25. package/modules/hosts/cursor/commands/wf-test.md +2 -2
  26. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  27. package/modules/hosts/opencode/commands/wf-design.md +5 -4
  28. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  29. package/modules/hosts/opencode/commands/wf-new-task.md +47 -25
  30. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  31. package/modules/hosts/opencode/commands/wf-review.md +2 -2
  32. package/modules/hosts/opencode/commands/wf-sync-hosts.md +7 -13
  33. package/modules/hosts/opencode/commands/wf-test.md +2 -2
  34. package/modules/hosts/trae/commands/wf-build.md +1 -1
  35. package/modules/hosts/trae/commands/wf-design.md +5 -4
  36. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  37. package/modules/hosts/trae/commands/wf-new-task.md +47 -25
  38. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  39. package/modules/hosts/trae/commands/wf-review.md +2 -2
  40. package/modules/hosts/trae/commands/wf-sync-hosts.md +7 -13
  41. package/modules/hosts/trae/commands/wf-test.md +2 -2
  42. package/package.json +1 -1
  43. package/src/cli.mjs +47 -4
  44. package/src/doctor.mjs +48 -3
  45. package/src/executor.mjs +262 -0
  46. package/src/executor.test.mjs +295 -0
  47. package/src/fresh-init.test.mjs +1 -1
  48. package/src/init.mjs +27 -3
  49. package/src/profiles.mjs +10 -3
  50. package/src/render.mjs +3 -3
  51. package/src/rule-budget.test.mjs +2 -2
  52. package/src/stack-profile.test.mjs +6 -0
  53. package/src/sync-hosts.mjs +37 -43
  54. package/src/sync-hosts.test.mjs +67 -56
  55. package/src/validate.mjs +6 -1
  56. package/src/validate.test.mjs +1 -1
  57. package/templates/.gitattributes +12 -1
  58. package/templates/AGENTS.md +3 -3
  59. package/templates/_agents/commands/build.md +1 -1
  60. package/templates/_agents/commands/design.md +5 -4
  61. package/templates/_agents/commands/maintain.md +1 -1
  62. package/templates/_agents/commands/new-task.md +47 -25
  63. package/templates/_agents/commands/plan.md +1 -1
  64. package/templates/_agents/commands/review.md +2 -2
  65. package/templates/_agents/commands/sync-hosts.md +7 -13
  66. package/templates/_agents/commands/test.md +2 -2
  67. package/templates/_agents/control-surfaces.txt +1 -1
  68. package/templates/_agents/engine-lock.json +334 -0
  69. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  70. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  71. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  72. package/templates/_agents/lane-surfaces.txt +5 -2
  73. package/templates/_agents/level-hints.txt +30 -0
  74. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  75. package/templates/_agents/mechanism-registry.md +27 -0
  76. package/templates/_agents/rule-budgets.txt +2 -2
  77. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  78. package/templates/_agents/scripts/agg-delegations.test.mjs +5 -3
  79. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  80. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  81. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  82. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  83. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  84. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  85. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  86. package/templates/_agents/scripts/check-engine-integrity.mjs +59 -127
  87. package/templates/_agents/scripts/check-engine-integrity.test.mjs +33 -45
  88. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  89. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  90. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  91. package/templates/_agents/scripts/check-lane-surface.mjs +74 -25
  92. package/templates/_agents/scripts/check-lane-surface.test.mjs +71 -5
  93. package/templates/_agents/scripts/check-ledger-invariant.mjs +427 -78
  94. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +438 -4
  95. package/templates/_agents/scripts/check-loop-rev.test.mjs +13 -8
  96. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  97. package/templates/_agents/scripts/check-loop.mjs +264 -75
  98. package/templates/_agents/scripts/check-loop.test.mjs +587 -61
  99. package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
  100. package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
  101. package/templates/_agents/scripts/check-registry.mjs +45 -0
  102. package/templates/_agents/scripts/check-scope.mjs +96 -0
  103. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  104. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  105. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  106. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  107. package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
  108. package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
  109. package/templates/_agents/scripts/confirm-doc.mjs +136 -31
  110. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  111. package/templates/_agents/scripts/doctor.test.mjs +44 -1
  112. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  113. package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
  114. package/templates/_agents/scripts/fill-intent.mjs +7 -5
  115. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  116. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  117. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  118. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  119. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  120. package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
  121. package/templates/_agents/scripts/fixture-scripts.test.mjs +112 -0
  122. package/templates/_agents/scripts/gate-checklist.mjs +10 -9
  123. package/templates/_agents/scripts/gate-checklist.test.mjs +2 -11
  124. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  125. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  126. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  127. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  128. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  129. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +16 -14
  130. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  131. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -36
  132. package/templates/_agents/scripts/git-core.mjs +45 -0
  133. package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
  134. package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
  135. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  136. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  137. package/templates/_agents/scripts/kb-search.mjs +19 -4
  138. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  139. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  140. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  141. package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
  142. package/templates/_agents/scripts/ledger-loader.mjs +108 -0
  143. package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
  144. package/templates/_agents/scripts/ledger-prune.mjs +96 -0
  145. package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
  146. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  147. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  148. package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
  149. package/templates/_agents/scripts/level-criteria.mjs +231 -0
  150. package/templates/_agents/scripts/level-criteria.test.mjs +281 -0
  151. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  152. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  153. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  154. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  155. package/templates/_agents/scripts/pipeline-run.mjs +10 -24
  156. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  157. package/templates/_agents/scripts/policy.mjs +75 -0
  158. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  159. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  160. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  161. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  162. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  163. package/templates/_agents/scripts/run-tests.mjs +99 -0
  164. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  165. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  166. package/templates/_agents/scripts/stage-gates.mjs +38 -21
  167. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  168. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  169. package/templates/_agents/scripts/verify.mjs +135 -58
  170. package/templates/_agents/scripts/verify.test.mjs +156 -47
  171. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  172. package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
  173. package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
  174. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  175. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  176. package/templates/_agents/scripts/workflows-check.mjs +1 -1
  177. package/templates/_agents/scripts-test-exempt.txt +1 -0
  178. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  179. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  180. package/templates/_agents/workflow-enums.txt +8 -1
  181. package/templates/_agents/workflows/pipeline-closing.md +4 -5
  182. package/templates/_githooks/pre-commit +109 -120
  183. package/templates/_githooks/pre-merge-commit +27 -23
  184. package/templates/_githooks/pre-push +7 -6
  185. package/templates/_github/workflows/kit-ci.yml +4 -3
  186. package/templates/workflow/README.md +23 -5
  187. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  188. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  189. package/templates/workflow/papercuts.md +2 -0
  190. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  191. package/templates/workflow/regression-checklist.md +1 -0
  192. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  193. package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -89
  194. package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -89
  195. package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -89
  196. package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -89
  197. package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -90
  198. package/templates/_agents/commands/source-sync-check.md +0 -103
  199. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  200. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  201. package/templates/_agents/scripts/check-ledger.mjs +0 -109
  202. package/templates/_agents/scripts/check-ledger.test.mjs +0 -199
  203. package/templates/_agents/scripts/rule-budget.sh +0 -74
  204. package/templates/_agents/scripts/source-sync-check.mjs +0 -156
  205. package/templates/_agents/scripts/source-sync-check.test.mjs +0 -286
  206. package/templates/_agents/workflows/source-sync-repair.md +0 -30
@@ -7,39 +7,44 @@
7
7
  // ② CI 的机器门跑的是**被测分支自己的** `.agents/scripts/check-loop.mjs`(ci.yml:42)——被改坏的
8
8
  // 裁判验证自己的篡改,无外部参照,CI 全绿。
9
9
  //
10
- // 本门的做法:把「门禁本体当前的硬拦面」**冻结进一个入库基线** `.agents/engine-lock.json`,
11
- // 判定 = HEAD 盘面向量 ↔ 基线逐项比对(哈希 + 硬拦单位数)。它不是签名、也不宣称不可伪造——
12
- // 它把「静默改弱」变成「必须显式声明的改弱」:
10
+ // 本门的做法:把「门禁本体当前的硬拦面」**冻结进一个入库基线** `templates/_agents/engine-lock.json`,
11
+ // 判定 = 包源盘面向量 ↔ 基线逐项比对(哈希 + 硬拦单位数)。它不是签名、也不宣称不可伪造——
12
+ // 它把「静默改弱」变成「必须显式 --update 的改弱」:
13
13
  // · 硬拦单位数**下降**(删 blocker / 删 exit 1 / 从面清单删行 / 从 CI 删一次门调用)→ 无条件 BLOCK;
14
- // · 内容变了但硬拦面没缩 → 须有一份**活跃 L2/L3 入口文档正文点名该文件**,否则 BLOCK;
15
- // · 基线本身在受检面内(engine-lock.json 已入 control-surfaces,装户径与包源径两面),
16
- // 改基线同样要过入口门。
17
- // 于是绕过路径从「编辑一个文件」抬到「编辑两个文件 + 立一份 L2 文档 + 落一条确认台账 + 提交」——
18
- // 每一环都在 git 历史与台账里留痕,可事后对质。诚实边界:`--no-verify` / 不挂 hooksPath /
19
- // 删掉本脚本仍可绕过本地门,兜底 = CI 复跑(本脚本被 ci.yml 调用)+ 分支保护;本地信任边界内
14
+ // · 内容变了但硬拦面没缩 → 同样 BLOCK,直到跑 --update 把新盘面显式入账;
15
+ // · 基线本身在受检面内(control-surfaces 列了 templates/_agents/engine-lock.json),
16
+ // 改基线同样要过本门(对基线的改动 = 受检面内容变更 → 未入新基线即拦)。
17
+ // 诚实边界:`--no-verify` / 不挂 hooksPath / 删掉本脚本仍可绕过本地门,兜底 = CI 复跑
18
+ // (本脚本被 .github/workflows 的 engine-integrity job 调用)+ 分支保护;本地信任边界内
20
19
  // 不可机器防这一点不掩盖(沿 policy.mjs#ledgerChainHash 与 README 确认门口径)。
21
20
  //
21
+ // **无声明式豁免(2026-10-13 移除入口退路)**:此前本门有两档豁免——本地层「正文提及即认」、
22
+ // 锚层「活跃 L2/L3 入口在 §改动面 点名」,参照物是 `<root>/workflow/{intents,incidents}`。
23
+ // 但 1.7.15「卸掉同仓双身份」后**包源仓已无该目录**(装户端内容清理),退路在本仓结构性不可达:
24
+ // 门消息让包源维护者去「立 L2 入口」,而立不了——不可达的能力等于误导。故整体移除(连带
25
+ // T4b/T4c/T4d 与锚层严格档测试):本仓只认 --update 这一条显式演进路径,语义更少、更可核对。
26
+ // **代价(诚实记录)**:跨提交锚只比对**硬拦单位数**、不比 sha,故「改写某检查的语义但保住
27
+ // exit 1 计数 + --update」这一形态两层都绿。补法是让锚层也比对 sha,代价是每次合法演进都需一份
28
+ // 显式凭据(改判据面,未决)。
29
+ //
22
30
  // 可选外部锚两条(都是「不受被测对象控制的参照物」,缺省不启用=无外部副本/无历史时不误报):
23
31
  // --against <目录> 逐字比对包源件与外部副本(装户/发布仓:node_modules/agentic-flow-kit)
24
32
  // --against-ref <ref> 跨提交锚(破 CI 自指的关键一环):从**历史 ref** 读基线(git show <ref>:<lock>),
25
33
  // 与盘面硬拦单位数做单调比较。本地门只比「盘面 ↔ 同分支基线」——恶意分支可同时改弱门禁并 --update
26
34
  // 重写基线,两者自洽、CI 全绿(「裁判给自己判无罪」的第二形态)。跨提交锚的参照物在**祖先提交**里,
27
35
  // 改它要 rewrite history + 强推(分支保护拦住),CI 由此获得一个不受被测分支控制的参照。
28
- // 合法收缩(退役一道门)不是一步死路:本地层宽松档(正文提及即认),锚层**严格档**——须活跃
29
- // L2/L3 入口在 §改动面 段落点名该文件(正文顺手提及不豁免;否则存量无关 L2 文档可替任意改弱背书)。
36
+ // 硬拦面收缩在本层**无豁免出口**:锚的参照物分支改不动,只能改判据基线并走评审。
30
37
  // ref 不可解析 / 该 ref 无基线(首个入账提交)→ 跳过并报 ℹ️(锚缺失不误报)。
31
38
  //
32
- // 判据原则(沿 check-lane-surface):只用一手事实——盘面字节哈希、可数单位、文档正文点名;
33
- // 不从时间戳反推过程。
39
+ // 判据原则(沿 check-lane-surface):只用一手事实——盘面字节哈希与可数单位;
40
+ // 不从时间戳反推过程、不靠文档正文为改动背书。
34
41
  // 用法:node check-engine-integrity.mjs [--root <dir>] [--update] [--against <dir>] [--against-ref <ref>] [--quiet]
35
- // --update 按当前盘面重生成基线(合法演进的最后一步;本身是控制面改动,须与入口文档同批提交)
36
- // 退出码:0 通过(含基线缺失=未启用);1 阻断(stderr 出 [标签] 与修法);2 用法错误(如 --against 目录不存在)
42
+ // --update 按当前盘面重生成基线(合法演进的唯一路径:演进=显式改基线,不存在自动放行)
43
+ // 退出码:0 通过(含基线缺失=未启用);1 阻断(stderr 出 [标签] 与修法,或目标根不是包源仓);2 用法错误(如 --against 目录不存在)
37
44
  import fs from 'node:fs';
38
45
  import path from 'node:path';
39
46
  import { createHash } from 'node:crypto';
40
47
  import { spawnSync } from 'node:child_process';
41
- import { laneOfEntry, laneOfDoc } from './stage-gates.mjs';
42
- import { loadKitPolicy } from './policy.mjs';
43
48
 
44
49
  const argv = process.argv.slice(2);
45
50
  const argOf = (k) => {
@@ -53,24 +58,19 @@ const ROOT = argOf('--root') || (() => {
53
58
  const r = spawnSync('git', ['-C', process.cwd(), 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });
54
59
  return r.status === 0 ? String(r.stdout || '').trim() : process.cwd();
55
60
  })();
56
- // 引擎基线路径双布局(装户端内容清理后):盘面已有基线按其径读;两处皆无时按**仓库布局**定
57
- // 落点——.agents/ 在 = 装户径;否则 templates/_agents/ 在 = 包源径(--update 才不会重建已清理的装户目录)。
58
- // ⚠️ 包源径在 sync 下发面内(render.mjs 只排除 .agents/cache):包源基线的键是 templates/_agents/scripts/…
59
- // 路径,装户盘面是 .agents/scripts/…——一旦把包源基线入库,sync 会把它下发进每个装户的 .agents/,
60
- // 命名空间错配 → 全员误报「门禁件消失 / 未入基线」。纯包源仓**不得入库包源基线**;要引擎仓自证
61
- // 完整性,须把基线放到 sync 扫描面之外(如仓库根)再接线 CI,且其豁免出口(L2 入口)在纯包源仓
62
- // 无 workflow/ 的情况下无处可立——这是设计约束,不是待修缺陷。
63
- const LOCK_CANDIDATES = ['.agents/engine-lock.json', 'templates/_agents/engine-lock.json'];
64
- const resolveLock = () => {
65
- for (const rel of LOCK_CANDIDATES) {
66
- if (fs.existsSync(path.join(ROOT, rel))) return { rel, abs: path.join(ROOT, rel) };
67
- }
68
- const fallback = fs.existsSync(path.join(ROOT, '.agents'))
69
- ? LOCK_CANDIDATES[0]
70
- : LOCK_CANDIDATES[1];
71
- return { rel: fallback, abs: path.join(ROOT, fallback) };
72
- };
73
- const { rel: LOCK_REL, abs: LOCK_ABS } = resolveLock();
61
+ // 只认包源。基线固定 templates/_agents/engine-lock.json。受检面只扫包源模板
62
+ // (scripts / _githooks / templates/_github / .github/workflows / templates/AGENTS.md / templates/_agents/*.txt)。
63
+ // `.github/workflows/*.yml` 是包源自身的 CI 编排——本门的调用点在那儿,不纳入就等于门可被无声关闭。
64
+ // 不扫 .agents、不扫项目根 .githooks、不按「有没有 .agents」猜布局。
65
+ // 无 templates/_agents 时所有模式都拒绝:静默扫到 0 个文件会看起来像通过。
66
+ // render.mjs 排除 .agents/engine-lock.json,避免把包源基线下发进装户。
67
+ const pkgAgents = path.join(ROOT, 'templates', '_agents');
68
+ if (!fs.existsSync(pkgAgents)) {
69
+ console.error(`check-engine-integrity 只在包源仓运行(需要 templates/_agents)。目标根 ${ROOT} 不是包源仓。`);
70
+ process.exit(1);
71
+ }
72
+ const LOCK_REL = 'templates/_agents/engine-lock.json';
73
+ const LOCK_ABS = path.join(ROOT, LOCK_REL);
74
74
 
75
75
  // ---- 受检面(单源:本函数;基线与比对同一份清单,防「基线少记一项」的静默空洞)----
76
76
  const gateFiles = () => {
@@ -86,18 +86,20 @@ const gateFiles = () => {
86
86
  }
87
87
  };
88
88
  addDir('templates/_agents/scripts', (r) => /\.mjs$/.test(r) && !r.endsWith('.test.mjs'));
89
- addDir('.agents/scripts', (r) => /\.mjs$/.test(r) && !r.endsWith('.test.mjs'));
90
89
  addDir('templates/_githooks');
91
- addDir('.githooks');
92
- addDir('.agents/hooks');
93
- addDir('.github/workflows', (r) => r.endsWith('.yml'));
94
90
  addDir('templates/_github/workflows', (r) => r.endsWith('.yml'));
91
+ // 包源自身的机器门编排(.github/workflows/*.yml)。**刻意纳入**:本门是「唯一能防改弱门禁本体」的检查,
92
+ // 而门要成为门就得有人调用它——调用点只存在于 CI 编排里。不纳入的话,删掉编排里的本门调用
93
+ // (或整个 job)没有任何检查会发现:被保护面之外的空白,正是它自身失效的那条路径。
94
+ addDir('.github/workflows', (r) => r.endsWith('.yml'));
95
95
  for (const f of ['lane-surfaces.txt', 'control-surfaces.txt', 'workflow-enums.txt', 'workflow-modules.txt',
96
- 'scripts-test-exempt.txt', 'rule-budgets.txt', 'metric-claims.txt']) {
97
- add(`.agents/${f}`);
96
+ 'scripts-test-exempt.txt', 'rule-budgets.txt', 'metric-claims.txt',
97
+ // level-hints.txt(2026-10-13 flow-kit-route 补录):风险级别的**路径下限**清单,直接驱动
98
+ // flow-kit route 定级结果,且语法错会 fail-loud 抛 LEVEL_HINTS_INVALID 改判级结论。
99
+ // 与 lane-surfaces.txt / workflow-modules.txt 同属「喂判据的输入面」,此前漏在受检面外。
100
+ 'level-hints.txt']) {
98
101
  add(`templates/_agents/${f}`);
99
102
  }
100
- add('AGENTS.md');
101
103
  add('templates/AGENTS.md');
102
104
  return [...out].sort();
103
105
  };
@@ -112,7 +114,10 @@ const hardUnits = (rel, text) => {
112
114
  const count = (re) => (text.match(re) || []).length;
113
115
  if (/\.m?c?js$/.test(rel) && !rel.endsWith('.test.mjs')) return count(/blockers\.push\(/g) + count(/process\.exit\(1\)/g);
114
116
  if (/^(\.githooks|templates\/_githooks)\//.test(rel) || /\.sh$/.test(rel) || rel.endsWith('local-pre-commit')) {
115
- return count(/\bexit\s+1\b/g);
117
+ // 硬拦单位数 = 钩子内可阻断提交的退出点。2026-10-13 门禁去 sh 化(B2):无扩展名钩子改 CommonJS 后
118
+ // 硬拦形态从 sh 的 `exit 1` 变为 process.exit(非零字面/状态表达式)——两种形态并计,sh 侧计数不变
119
+ // (sh 无 process.exit;exit(0) 成功路径不计)。此为改判据基线的合法演进(本文件头口径),与 CHANGELOG 同批。
120
+ return count(/\bexit\s+1\b/g) + count(/process\.exit\((?!0\b)/g);
116
121
  }
117
122
  if (rel.endsWith('.yml')) return count(/node [^\s]*scripts\/[^\s]*\.(mjs|cjs|sh)/g); // CI 里每道门的调用
118
123
  if (/\.(txt|json)$/.test(rel)) {
@@ -135,71 +140,6 @@ const readJson = (p) => {
135
140
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
136
141
  };
137
142
 
138
- // 活跃 L2/L3(泳道 high)入口集合:一次扫描,宽松/严格两档判据共用同一份盘面事实
139
- const HIGH_ENTRIES = (() => {
140
- const ACTIVE = { intent: new Set(['draft', 'approved']), incident: new Set(['open', 'fixed']) };
141
- const fmGet = (lines, key) => {
142
- for (let i = 1; i < lines.length; i++) {
143
- if (/^---\s*$/.test(lines[i])) break;
144
- const m = lines[i].match(new RegExp(`^${key}:\\s*(.*)$`));
145
- if (m) return m[1].trim();
146
- }
147
- return '';
148
- };
149
- const pol = loadKitPolicy(ROOT);
150
- const cache = [];
151
- for (const [dir, kind] of [['intents', 'intent'], ['incidents', 'incident']]) {
152
- const dp = path.join(ROOT, 'workflow', dir);
153
- if (!fs.existsSync(dp)) continue;
154
- for (const f of fs.readdirSync(dp)) {
155
- if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
156
- const abs = path.join(dp, f);
157
- let lines;
158
- try { lines = fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n').split('\n'); } catch { continue; }
159
- if (!/^---\s*$/.test(lines[0] || '')) continue;
160
- const st = fmGet(lines, '状态');
161
- if (!ACTIVE[kind].has(st)) continue;
162
- const level = fmGet(lines, '级别');
163
- const lane = kind === 'intent'
164
- ? laneOfEntry(level, fmGet(lines, 'risk_level'), pol.riskLevelSince, fmGet(lines, '日期'))
165
- : laneOfDoc(level);
166
- if (lane !== 'high') continue;
167
- cache.push({ rel: `workflow/${dir}/${f}`, text: lines.join('\n') });
168
- }
169
- }
170
- return cache;
171
- })();
172
-
173
- // 宽松档(本地层):整条 rel 或 basename 出现在正文即认——口径同检查 8 的「声明改动面 ↔ 提交文件」
174
- // 模糊匹配,宁松不假阳性;精确性由 confirm-doc 指纹与台账承担。
175
- const entryNamesFile = (fileRel) => {
176
- const base = path.basename(fileRel);
177
- return HIGH_ENTRIES.find((e) => e.text.includes(fileRel) || e.text.includes(base)) || null;
178
- };
179
-
180
- // 严格档(仅跨提交锚用):入口须在**声明改动面**段落点名该文件,而非正文任意一处提及。
181
- // 为什么分层:本地层的模糊放行代价低(内容漂移本就允许「声明过的演进」);锚层拦的是
182
- // 「硬拦单位数下降」——若一句顺手提及就能豁免,本仓实测三份**与本批无关的存量 L2 incident**
183
- // (2026-09-28-metric-claim-gate / 2026-10-03-plan-confirm-boiler 正文含 check-loop.mjs,
184
- // 2026-09-26-managed-ledger-adopt 含 pre-commit)即可替任意改弱行为背书,锚等于没有。
185
- // 严格档的误拦修法就是协议本意:把该文件写进入口的 §改动面。
186
- const entryDeclaresFile = (() => {
187
- const SECTION = /^[^\n]{0,40}改动面/;
188
- const declared = (text) => {
189
- const lines = text.split('\n');
190
- const out = [];
191
- for (let i = 0; i < lines.length; i++) {
192
- if (/^---\s*$/.test(lines[i]) || !SECTION.test(lines[i])) continue;
193
- // 含标题行本身(「§改动面 点名 x.mjs」是常见的一行式声明)与标题后到下一个 heading 前的全部正文
194
- for (let j = i; j < lines.length && (j === i || !/^\s*#{1,6}\s/.test(lines[j])); j++) out.push(lines[j]);
195
- }
196
- return out.join('\n');
197
- };
198
- return (fileRel) => {
199
- const base = path.basename(fileRel);
200
- return HIGH_ENTRIES.find((e) => { const s = declared(e.text); return s.includes(fileRel) || s.includes(base); }) || null;
201
- };
202
- })();
203
143
 
204
144
  if (UPDATE) {
205
145
  fs.mkdirSync(path.dirname(LOCK_ABS), { recursive: true });
@@ -211,7 +151,7 @@ if (UPDATE) {
211
151
  console.log(`引擎基线已更新:${LOCK_REL}(受检面 ${Object.keys(lock.files).length} 个文件)`);
212
152
  if (changed.length) console.log(` 本次入账为改动件:${changed.length} 个`);
213
153
  if (removed.length) console.log(` 本次移除(文件已不在盘):${removed.join(' ')}`);
214
- console.log(' ⚠️ 基线更新本身是控制面改动:须与点名这些文件的 L2 入口文档同批提交(否则检查 C / 本门默认模式会拦)');
154
+ console.log(' ⚠️ 基线更新本身是控制面改动:须与该次演进的 CHANGELOG 记录同批提交(改基线 = 受检面内容变更,不入新基线即被本门拦)');
215
155
  process.exit(0);
216
156
  }
217
157
 
@@ -223,14 +163,10 @@ for (const rel of gateFiles()) current[rel] = { sha256: shaOf(path.join(ROOT, re
223
163
  // ---- 跨提交锚(先于本地基线判定:恶意分支把盘面 engine-lock.json 删掉也逃不掉这一层——参照物读自 git 对象库)----
224
164
  if (argOf('--against-ref')) {
225
165
  const ref = argOf('--against-ref');
226
- // 两候选径都试(装户径 / 包源径)——纯包源仓的基线在 templates/_agents/ 下,锚须读得到
227
166
  let anchor = null;
228
- for (const rel of LOCK_CANDIDATES) {
229
- const r = spawnSync('git', ['-C', ROOT, 'show', `${ref}:${rel}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
230
- try { anchor = JSON.parse(String(r.stdout || '')); } catch { /* 该候选径在此 ref 无基线 → 试下一个 */ }
231
- if (anchor && anchor.files) break;
232
- anchor = null;
233
- }
167
+ const r = spawnSync('git', ['-C', ROOT, 'show', `${ref}:${LOCK_REL}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
168
+ try { anchor = JSON.parse(String(r.stdout || '')); } catch { /* 该 ref 无基线 */ }
169
+ if (!anchor || !anchor.files) anchor = null;
234
170
  if (!anchor) {
235
171
  if (!QUIET) console.log(`ℹ️ 跨提交锚缺失(${ref} 无 ${LOCK_REL})——该层跳过(首个入账提交/浅克隆)`);
236
172
  } else {
@@ -238,16 +174,13 @@ if (argOf('--against-ref')) {
238
174
  const got = current[rel];
239
175
  if (!got) { problems.push(`[门禁件消失(跨提交锚)] ${rel} 在 ${ref} 的基线内但盘面不存在——删除一道门等于禁用该检查`); continue; }
240
176
  if (typeof was.hard !== 'number' || got.hard >= was.hard) continue;
241
- const e = entryDeclaresFile(rel);
242
- if (!e) {
243
- problems.push(`[硬拦面收缩(跨提交锚)] ${rel} 硬拦单位数 ${was.hard}(见 ${ref})→ 盘面 ${got.hard},减少 ${was.hard - got.hard}——祖先提交是参照物,本分支改不动它;确需退役该判据须有活跃 L2/L3 入口在**声明改动面**段落点名该文件(正文顺手提及不算——存量无关文档会替任意改弱行为背书)`);
244
- }
177
+ problems.push(`[硬拦面收缩(跨提交锚)] ${rel} 硬拦单位数 ${was.hard}(见 ${ref})→ 盘面 ${got.hard},减少 ${was.hard - got.hard}——祖先提交是参照物,本分支改不动它。本门无声明式豁免出口(2026-10-13 移除:入口退路在本仓结构性不可达,详见文件头),收缩一律拦`);
245
178
  }
246
179
  }
247
180
  }
248
181
 
249
182
  if (!lock || !lock.files) {
250
- if (!QUIET) console.log(`ℹ️ 引擎基线未启用(无 ${LOCK_REL})——跑 node .agents/scripts/check-engine-integrity.mjs --update 生成(跨提交锚与外部副本锚照常执行)`);
183
+ if (!QUIET) console.log(`ℹ️ 引擎基线未启用(无 ${LOCK_REL})——跑 node templates/_agents/scripts/check-engine-integrity.mjs --update 生成(跨提交锚与外部副本锚照常执行)`);
251
184
  } else {
252
185
  for (const [rel, want] of Object.entries(lock.files)) {
253
186
  const got = current[rel];
@@ -255,14 +188,13 @@ if (!lock || !lock.files) {
255
188
  if (!got) { problems.push(`[门禁件消失] ${rel} 在基线内但盘面不存在——删除一道门等于禁用该检查`); continue; }
256
189
  if (got.sha256 === want.sha256) continue;
257
190
  if (typeof want.hard === 'number' && got.hard < want.hard) {
258
- problems.push(`[硬拦面收缩] ${rel} 硬拦单位数 ${want.hard} → ${got.hard}(减少 ${want.hard - got.hard})——删 blocker/exit/判据行的结构性形态,无论是否声明均拦`);
191
+ problems.push(`[硬拦面收缩] ${rel} 硬拦单位数 ${want.hard} → ${got.hard}(减少 ${want.hard - got.hard})——删 blocker/exit/判据行的结构性形态,一律拦`);
259
192
  } else {
260
- const e = entryNamesFile(rel);
261
- if (!e) problems.push(`[控制面改动无入口] ${rel} 内容与基线不符(${name} 硬拦单位数 ${want.hard} → ${got.hard})——无活跃 L2/L3 入口正文点名该文件`);
193
+ problems.push(`[控制面改动未入基线] ${rel} 内容与基线不符(${name} 硬拦单位数 ${want.hard} → ${got.hard})——门禁本体的每次演进都要显式更新基线,不存在「声明过的改动」自动放行`);
262
194
  }
263
195
  }
264
196
  for (const rel of Object.keys(current)) {
265
- if (!lock.files[rel]) problems.push(`[门禁件未入基线] ${rel} 是新出现的受检文件——合法新增判据须跑 --update 并把基线更新与入口文档同批提交`);
197
+ if (!lock.files[rel]) problems.push(`[门禁件未入基线] ${rel} 是新出现的受检文件——合法新增判据须跑 --update 入账`);
266
198
  }
267
199
  }
268
200
 
@@ -280,8 +212,8 @@ if (argOf('--against')) {
280
212
 
281
213
  if (problems.length) {
282
214
  console.error(`引擎完整性 — HARD-BLOCK:\n\n${problems.map((p) => `- ${p}`).join('\n')}\n`);
283
- console.error(` 修法:确属门禁演进 → 立 L2 入口(正文 §改动面 点名上述文件)+ 判据更新后跑 node .agents/scripts/check-engine-integrity.mjs --update,三者同批提交`);
284
- console.error(' 修法:非演进(误改/被他人改动)→ git diff 核对并回退,勿改基线绕过(基线本身在控制面清单内)');
215
+ console.error(` 修法:确属门禁演进 → 跑 node templates/_agents/scripts/check-engine-integrity.mjs --update 入账,并与 CHANGELOG 记录同批提交(提交评审即对质场;本门无声明式豁免出口,见文件头)`);
216
+ console.error(' 修法:非演进(误改/被他人改动)→ git diff 核对并回退,勿改基线绕过(基线本身在控制面清单内,改它同样是受检面变更)');
285
217
  process.exit(1);
286
218
  }
287
219
  if (!QUIET) console.log(`✅ 引擎完整性:受检面 ${Object.keys(current).length} 个文件(硬拦面无收缩${lock && lock.files ? '、盘面与基线一致' : '、本地基线未启用'})`);
@@ -37,8 +37,8 @@ const mkfix = ({ withEntry = false, entryNames = 'gate.mjs' } = {}) => {
37
37
  const d = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-integrity-test-'));
38
38
  w(d, 'templates/_agents/scripts/gate.mjs',
39
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');
40
+ w(d, 'templates/_githooks/pre-commit', '#!/bin/sh\nif ! node check.mjs; then\n echo blocked >&2\n exit 1\nfi\nexit 0\n');
41
+ w(d, 'templates/AGENTS.md', '# 协议\n- 规则一\n- 规则二\n');
42
42
  if (withEntry) {
43
43
  w(d, 'workflow/intents/2026-10-09-ctrl.md',
44
44
  `---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:templates/_agents/scripts/${entryNames}\n`);
@@ -56,14 +56,13 @@ const mkfix = ({ withEntry = false, entryNames = 'gate.mjs' } = {}) => {
56
56
  {
57
57
  const d = mkfix();
58
58
  const u = run(d, ['--update']);
59
- // fixture 无 .agents/ → 基线落包源径(resolveLock 布局回落,见 T9/T10)
60
59
  const lockPath = path.join(d, 'templates', '_agents', 'engine-lock.json');
61
60
  let lock = null;
62
61
  try { lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch { /* 断言兜 */ }
63
62
  const r = run(d);
64
63
  check('T2a --update → exit 0 且基线入账受检面', u.code === 0 && !!lock && !!lock.files['templates/_agents/scripts/gate.mjs'], u.out);
65
64
  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']));
65
+ check('T2c 钩子入面 hard=1(exit 1 计数)', !!lock && lock.files['templates/_githooks/pre-commit'].hard === 1, JSON.stringify(lock && lock.files['templates/_githooks/pre-commit']));
67
66
  check('T2d 盘面未变 → exit 0 且报「与基线一致」', r.code === 0 && r.out.includes('盘面与基线一致'), r.out);
68
67
  }
69
68
 
@@ -78,36 +77,31 @@ const mkfix = ({ withEntry = false, entryNames = 'gate.mjs' } = {}) => {
78
77
  check('T3b 有活跃 L2 入口仍拦(收缩无豁免出口)', r.code === 1 && r.out.includes('[硬拦面收缩]'), r.out);
79
78
  }
80
79
 
81
- // --- T4 非收缩改动:无入口拦、有入口放行 ---
80
+ // --- T4 非收缩改动:内容漂移即拦(2026-10-13 移除入口退路后无豁免出口)---
82
81
  {
83
82
  const d = mkfix();
84
83
  run(d, ['--update']);
85
84
  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);
85
+ const drift = run(d);
86
+ check('T4a 内容漂移(硬拦面未缩)→ [控制面改动未入基线]',
87
+ drift.code === 1 && drift.out.includes('[控制面改动未入基线]') && drift.out.includes('gate.mjs'), drift.out);
88
+ check('T4a 修法指向 --update(不再宣传不可达的「立 L2 入口」)',
89
+ drift.out.includes('--update') && !drift.out.includes('立 L2 入口'), drift.out);
90
+ check('T4a --update 入账后放行', run(d, ['--update']).code === 0 && run(d).code === 0);
102
91
  }
103
- // --- T4d 入口非活跃(done 已关单)→ 不算点名 ---
92
+ // --- T4e 入口面即便存在也不豁免(锁死新口径)---
93
+ // 1.7.15 卸掉同仓双身份后,包源仓没有 workflow/intents —— 旧「立入口即放行」退路在本仓不可达。
94
+ // 删掉该能力后,即使有人在包源仓手工造出入口文档点名该件,**也不得**成为放行口:
95
+ // 否则一个 mkdir 就把门重新打开,等于用未验证的路径恢复未验证的能力。
104
96
  {
105
- const d = mkfix({ withEntry: false });
97
+ const d = mkfix();
106
98
  run(d, ['--update']);
107
99
  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');
100
+ w(d, 'workflow/intents/2026-10-09-ctrl.md',
101
+ '---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:gate.mjs\n');
109
102
  const r = run(d);
110
- check('T4d done 入口不点名(活跃口径:draft/approved)', r.code === 1 && r.out.includes('[控制面改动无入口]'), r.out);
103
+ check('T4e 活跃 L2 入口点名该件 → 仍拦(本门无声明式豁免出口)',
104
+ r.code === 1 && r.out.includes('[控制面改动未入基线]'), r.out);
111
105
  }
112
106
 
113
107
  // --- T5 门禁件消失 / 新增件未入基线 ---
@@ -158,26 +152,20 @@ if (haveGit) {
158
152
  const anchored = run(d, ['--against-ref', 'HEAD']);
159
153
  check('T7b 跨提交锚(祖先提交基线)出账 [硬拦面收缩(跨提交锚)]',
160
154
  anchored.code === 1 && anchored.out.includes('[硬拦面收缩(跨提交锚)]') && /3(见 HEAD)→ 盘面 2/.test(anchored.out), anchored.out);
161
- // 合法退役出口:活跃 L2 入口点名该件
155
+ // T7c/T7e(2026-10-13 移除入口退路后删除):原为「活跃 L2 入口点名 → 锚层放行」与「严格档:
156
+ // 模糊提及不豁免」两条断言。二者均依赖 workflow/intents 入口面,而该面在本仓结构性不可达
157
+ // (1.7.15 卸掉同仓双身份),故能力与测试一并移除——锚层的收缩判定现在**无豁免出口**。
158
+ // T7c 替代断言:造出入口文档点名该件,锚层仍须拦(锁死「无豁免」不随时间被悄悄恢复)
162
159
  w(d, 'workflow/intents/2026-10-09-retire.md',
163
160
  '---\n状态: approved\n级别: L2\n日期: 2026-10-09\n模块: gate\n---\n# INTENT\n改动面:templates/_agents/scripts/gate.mjs(退役 B 判据)\n');
164
161
  git(d, ['add', '-A']);
165
- git(d, ['commit', '-q', '-m', 'docs(gate): L2 入口留痕']);
162
+ git(d, ['commit', '-q', '-m', 'docs(gate): 造入口文档(应不构成放行口)']);
166
163
  const declared = run(d, ['--against-ref', 'HEAD~1']);
167
- check('T7c 声明过的收缩(L2 入口点名)→ 跨提交锚放行', declared.code === 0, declared.out);
164
+ check('T7c 入口文档点名该件 → 锚层仍拦(本门无声明式豁免出口)',
165
+ declared.code === 1 && declared.out.includes('[硬拦面收缩(跨提交锚)]'), declared.out);
168
166
  // 锚缺失(首个入账提交之前)→ 跳过不误报
169
167
  const missing = run(d, ['--against-ref', 'HEAD~9']);
170
168
  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
169
  } else {
182
170
  console.log('SKIP T7 跨提交锚(未找到 git)');
183
171
  }
@@ -190,18 +178,18 @@ if (haveGit) {
190
178
  check('T8 --quiet 通过时不打印 ✅ 行', r.code === 0 && !r.out.includes('✅'), r.out);
191
179
  }
192
180
 
193
- // --- T9 装户布局:.agents/ 在 → 基线落装户径,不新建 templates/_agents/engine-lock.json ---
181
+ // --- T9 非包源根:拒绝,不写基线、不建 templates/_agents ---
194
182
  {
195
183
  const d = fs.mkdtempSync(path.join(os.tmpdir(), 'engine-integrity-test-'));
196
184
  w(d, '.agents/scripts/gate.mjs', 'blockers.push(`A`);\nblockers.push(`B`);\nif (bad) process.exit(1);\n');
197
185
  w(d, 'AGENTS.md', '# 协议\n- 规则一\n');
198
186
  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')), '');
187
+ const plain = run(d);
188
+ check('T9a 非包源根 --update 拒绝', u.code !== 0 && /只在包源仓运行/.test(u.out), u.out);
189
+ check('T9b 非包源根默认模式同样拒绝', plain.code !== 0 && /只在包源仓运行/.test(plain.out), plain.out);
190
+ check('T9c 不写 .agents/engine-lock.json,不建 templates/_agents',
191
+ !fs.existsSync(path.join(d, '.agents', 'engine-lock.json'))
192
+ && !fs.existsSync(path.join(d, 'templates', '_agents')), '');
205
193
  fs.rmSync(d, { recursive: true, force: true });
206
194
  }
207
195
 
@@ -0,0 +1,56 @@
1
+ // check-funnel.mjs — 检查 26:闭环漏斗「协议前」占比动作阈值 [warning](2026-10-13 B6b-2 自
2
+ // Shipyard.Material check-loop-sections/s26-funnel-action.mjs 回灌;段体判据逐字对齐,出处沿革留痕
3
+ // 在包源仓 git 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 背景(funnel-action-threshold):workflow/metrics.md 闭环漏斗早就在记「协议前」(收口却无确认链)
5
+ // 件数,但该数据只有观测没有动作——门禁零触发有退役阈值、指标超额有对账,唯独「闭环被绕开」这一项
6
+ // 无任何阈值,等于默许可绕。本段补动作面:漏斗表里日历相邻两月协议前占比均 >30% → WARN 并要求一轮归因。
7
+ // 判据口径:
8
+ // - 占比 = 协议前 / 收口;收口 = 0 的月份无占比(0/0 不判),只按有分母的月参与相邻比对;
9
+ // - 「相邻」= nextMonth(前一月) === 当前月(行已按月升序)——单月波动(批量迁移 / 集中补录)不触发;
10
+ // - 阈值 30%:源仓与生成器 gen-workflow-metrics.mjs 的 LEGACY_RATIO_LIMIT 同值(单源两侧);
11
+ // kit 的 gen-workflow-metrics 不渲染占比单元格(裸整数形态),故 0.30 单点在本文件——若日后给
12
+ // 生成器加 ⚠️ 标记面,须恢复「同值单源两侧 + 夹具钉住」纪律。
13
+ // 纯 WARN advisory(沿 §25 先例):这是月度节奏的过程事实,不是每次提交的正确性事实——
14
+ // 拿它做 hard-block 会把月度问题钉成每日拦路,逼人绕的正是门自己。升格判据见 mechanism-registry §26 行。
15
+ // 数据源是生成物(gen-workflow-metrics.mjs 单源写入),单元格形如 `79(45%) ⚠️` 或存量裸 `79`——
16
+ // 解析只取首个整数,两种形态都认(生成器升级当月不会整表重算,混合形态必须可读)。
17
+ // ROOT 由 check-loop 供给:--rev 模式下 ROOT 指向该提交的临时 worktree,故本段在 pre-push --rev
18
+ // 与工作区两种模式下读的都是同一棵树。
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+
22
+ export function run(ctx) {
23
+ const { ROOT, WF, warnings } = ctx;
24
+ // --- 26. 闭环漏斗「协议前」占比动作阈值 [warning] ---
25
+ const RATIO_LIMIT = 0.30;
26
+ const metricsPath = path.join(ROOT, WF, 'metrics.md');
27
+ if (!fs.existsSync(metricsPath)) return; // 装户/新仓无快照 = 未启用该观测,静默跳过(沿 lane-surfaces「缺失即跳过」惯例)
28
+ const rows = [];
29
+ let inFunnel = false;
30
+ for (const line of fs.readFileSync(metricsPath, 'utf8').split(/\r?\n/)) {
31
+ if (/^##\s+闭环漏斗/.test(line)) { inFunnel = true; continue; }
32
+ if (inFunnel && /^##\s+/.test(line)) break; // 漏斗段到下一节为止
33
+ if (!inFunnel) continue;
34
+ const m = line.match(/^\|\s*(\d{4}-\d{2})\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*(\d+)/);
35
+ if (!m) continue; // 表头/分隔行
36
+ rows.push({ month: m[1], closed: Number(m[2]), legacy: Number(m[4]) });
37
+ }
38
+ if (rows.length < 2) return;
39
+ rows.sort((a, b) => a.month.localeCompare(b.month));
40
+ const ratioOf = (r) => (r.closed > 0 ? r.legacy / r.closed : null);
41
+ const nextMonthKey = (ym) => {
42
+ const [y, m] = ym.split('-').map(Number);
43
+ return m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, '0')}`;
44
+ };
45
+ for (let i = 1; i < rows.length; i++) {
46
+ const prev = rows[i - 1];
47
+ const cur = rows[i];
48
+ if (nextMonthKey(prev.month) !== cur.month) continue;
49
+ const ra = ratioOf(prev);
50
+ const rb = ratioOf(cur);
51
+ if (ra === null || rb === null) continue;
52
+ if (ra > RATIO_LIMIT && rb > RATIO_LIMIT) {
53
+ warnings.push(`- [WARN 漏斗协议前超标] ${prev.month} ${(ra * 100).toFixed(0)}% → ${cur.month} ${(rb * 100).toFixed(0)}%(阈值 ${(RATIO_LIMIT * 100).toFixed(0)}%,相邻两月)——闭环在被系统性绕开:跑一轮归因(哪一环在逼人绕路:确认门太重 / 级别判错 / 批量豁免滥用?),结论记 workflow/papercuts.md 或立 incident,并在 mechanism-registry 月度对账登记处置`);
54
+ }
55
+ }
56
+ }
@@ -26,7 +26,7 @@ import { spawnSync } from 'node:child_process';
26
26
  import { gateSeg, finishSegs, makeCollector, appendSegs } from './gate-seg.mjs';
27
27
 
28
28
  // 门禁 ROI 插桩(2026-10-08 gate-roi-metrics):本模块承载的检查 2/9/11/12/13 同样需要计入门禁成本
29
- // 统计——尤其检查 11(调两个生成器 --check 的 spawn 子进程)与检查 13(调 sh rule-budget.sh)是耗时大户,
29
+ // 统计——尤其检查 11(调两个生成器 --check 的 spawn 子进程)与检查 13(调 rule-budget.mjs)是耗时大户,
30
30
  // 不插桩就等于把最贵的两段排除在度量之外。collector 由 check-loop 传入(它持有全局 warnings/blockers),
31
31
  // 未传时跳过插桩(纯函数直测场景零副作用)。判据单源见 gate-seg.mjs。
32
32
 
@@ -43,7 +43,7 @@ export function runCheckHygiene(ctx) {
43
43
  // --- 2. 模板字段占位符残留 [warning](排除运行时文件名格式与协议样板,口径沿 sh 版)---
44
44
  markGate('2', '模板字段占位符残留');
45
45
  {
46
- const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L0 \/ L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
46
+ const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
47
47
  const boilerRe = /\.\.\/specs\/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('YYYY-MM-DD'\)|value-format="YYYY-MM-DD"|确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)/;
48
48
  // plan 确认节样板豁免(2026-10-03 plan-confirm-boiler):plan 模板「确认与复核」节自带
49
49
  // 「确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)」
@@ -145,13 +145,13 @@ export function runCheckHygiene(ctx) {
145
145
  }
146
146
  }
147
147
 
148
- // --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.sh;无 sh 环境静默跳过——advisory,pre-commit 硬拦兜底)---
148
+ // --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.mjs——2026-10-13 自 .sh 移植 node,恒可跑)---
149
149
  markGate('13', '常驻面体积预算');
150
150
  {
151
- const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.sh');
151
+ const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.mjs');
152
152
  const budgets = path.join(ROOT, '.agents', 'rule-budgets.txt');
153
153
  if (fs.existsSync(rb) && fs.existsSync(budgets)) {
154
- const r = spawnSync('sh', [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
154
+ const r = spawnSync(process.execPath, [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
155
155
  if (!r.error && r.status !== 0) {
156
156
  const head = `${r.stdout || ''}${r.stderr || ''}`.split('\n').slice(0, 5).map((l) => ' ' + l).join('\n');
157
157
  warnings.push(`- [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):\n${head}`);
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // check-index-incremental.mjs:未触及 workflow 文档则跳过;触及且 INDEX 与磁盘不一致则 exit 1;重生成后 exit 0。
3
+ import { spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+
9
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const HOOK = path.join(SCRIPT_DIR, '..', 'hooks', 'check-index-incremental.mjs');
11
+ const GEN = path.join(SCRIPT_DIR, 'gen-workflow-index.mjs');
12
+ const { stagedTouchesIndex } = await import(pathToFileURL(HOOK).href);
13
+
14
+ let pass = 0;
15
+ let fail = 0;
16
+ const check = (desc, cond, detail = '') => {
17
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
18
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
19
+ };
20
+
21
+ check('路径:四目录 md 与 INDEX.md 触发', stagedTouchesIndex([
22
+ 'workflow/plans/2026-10-12-a.md',
23
+ 'README.md',
24
+ ]));
25
+ check('路径:归档 md 触发', stagedTouchesIndex(['workflow/archive/2026-10/plans/2026-10-01-a.md']));
26
+ check('路径:INDEX.md 触发', stagedTouchesIndex(['workflow/INDEX.md']));
27
+ check('路径:delegations 与引擎脚本不触发', !stagedTouchesIndex([
28
+ 'workflow/delegations.md',
29
+ '.agents/scripts/gen-workflow-index.mjs',
30
+ 'backend/x.cs',
31
+ ]));
32
+
33
+ const git = (cwd, args) => spawnSync('git', ['-c', 'core.hooksPath=', ...args], { cwd, encoding: 'utf8' });
34
+ const runHook = (cwd) => spawnSync(process.execPath, [HOOK], { cwd, encoding: 'utf8' });
35
+
36
+ function initRepo() {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'index-incremental-'));
38
+ git(root, ['init']);
39
+ git(root, ['config', 'user.email', 'index-test@example.com']);
40
+ git(root, ['config', 'user.name', 'index-test']);
41
+ for (const d of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(root, 'workflow', d), { recursive: true });
42
+ fs.writeFileSync(path.join(root, 'workflow', 'plans', '2026-10-01-a.md'), '---\n状态: done\n级别: L1\n日期: 2026-10-01\n---\n# PLAN — 甲\n', 'utf8');
43
+ fs.writeFileSync(path.join(root, 'README.md'), 'x\n', 'utf8');
44
+ return root;
45
+ }
46
+
47
+ {
48
+ const root = initRepo();
49
+ git(root, ['add', 'README.md']);
50
+ const r = runHook(root);
51
+ check('未暂存 workflow 文档 → exit 0', r.status === 0, `${r.status}\n${r.stdout}${r.stderr}`);
52
+ fs.rmSync(root, { recursive: true, force: true });
53
+ }
54
+ {
55
+ const root = initRepo();
56
+ const gen = spawnSync(process.execPath, [GEN], { cwd: root, encoding: 'utf8' });
57
+ check('夹具生成 INDEX exit 0', gen.status === 0, gen.stderr);
58
+ git(root, ['add', 'workflow']);
59
+ const ok = runHook(root);
60
+ check('INDEX 与磁盘一致 → exit 0', ok.status === 0, `${ok.stdout}${ok.stderr}`);
61
+ fs.writeFileSync(path.join(root, 'workflow', 'plans', '2026-10-01-a.md'), '---\n状态: approved\n级别: L1\n日期: 2026-10-01\n---\n# PLAN — 甲\n', 'utf8');
62
+ git(root, ['add', 'workflow/plans/2026-10-01-a.md']);
63
+ const blocked = runHook(root);
64
+ check('状态变了但 INDEX 未重生成 → exit 1', blocked.status === 1, `${blocked.stdout}${blocked.stderr}`);
65
+ const regen = spawnSync(process.execPath, [GEN], { cwd: root, encoding: 'utf8' });
66
+ git(root, ['add', 'workflow/INDEX.md']);
67
+ const again = runHook(root);
68
+ check('重生成并暂存 INDEX 后 → exit 0', regen.status === 0 && again.status === 0, `${regen.stderr}${again.stdout}${again.stderr}`);
69
+ fs.rmSync(root, { recursive: true, force: true });
70
+ }
71
+
72
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
73
+ if (fail) process.exit(1);