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
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // run-tests — 引擎脚本测试统一入口(2026-10-08 agents-hygiene 批2;2026-10-09 并行化)
3
+ // 用途:一条命令跑 .agents/scripts/ 下全部 *.test.mjs 套件——逐个 node 直跑(与 test.md 逐条口径一致,
4
+ // 不引 node --test:既有套件是自研 check/「合计: PASS n / FAIL n」计数风格,非 node:test 风格)。
5
+ // 语义:套件并发执行(默认 6,--concurrency <N> 可调,1 = 等价旧串行;非正整数 fail-loud)——每套件
6
+ // stdout/stderr 缓冲、完成即回放(「[run-tests] ▶ 套件名」分隔头,避免并行交错不可读);
7
+ // stderr 仅红套件回放尾部 5 行防刷屏;聚合各套件「合计: PASS n / FAIL n」末次计数(无该行按 0 计);
8
+ // 任一套件 exit 非零 / spawn 失败 / 计数 FAIL>0 → 该套件记红;有红套件即 exit 1。
9
+ // 套件发现 = 目录内 *.test.mjs 字典序(非递归);回放按完成序(并行的自然结果,不影响裁决)。
10
+ // 红线:只做调度层并行,不改任何套件内部;并发默认 6(2026-10-09 实测:串行 2m47s → 并发4 2m05s →
11
+ // 并发6 1m48s,套件各异步 tmp fixture 无共享态)——Windows spawn 密集会放大瞬时抖动(同日实证一次
12
+ // git EBUSY 偶发红),故不取更高默认;套件内 git 调用已有 gitRetry 防护。
13
+ // 2026-10-10 复测定案(perf 系列):6→12 仅 -3~5%(114.4s → 111.8/108.4s,本机 12 核,两次 c12 全绿
14
+ // 无偶发红;噪声带 ±3s)——膝点在 4-6 间,并发档位已非杠杆:工作池地板 = check-loop.test 独跑
15
+ // 73.8s(占总时长 65%,234 次套件内顺序 spawn node)。再快须并行化套件内场景(动套件内部结构,
16
+ // 撞本红线),收益/风险另议;默认维持 6。
17
+ // 用法:node .agents/scripts/run-tests.mjs [--dir <目录>] [--only <文件名子串>] [--concurrency <N>]
18
+ // --dir 默认本脚本所在目录(测试注入钩子);--only 过滤文件名(调试单套件用,如 --only check-loop)。
19
+ // 测试:node .agents/scripts/run-tests.test.mjs(临时目录夹具:并行全绿聚合 / 红套件 / 崩溃套件 / --only / 空目录 / 并发数校验 / 并发1=串行)
20
+ // 沿革:2026-10-08 立档(串行);2026-10-09 并行化(sess_52d1d1be 路线图「串行→并行」真缺口)。
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { spawn } from 'node:child_process';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
27
+ const args = process.argv.slice(2);
28
+ const val = (flag) => (args.includes(flag) ? args[args.indexOf(flag) + 1] : null);
29
+ const dir = val('--dir') || SCRIPT_DIR;
30
+ const only = val('--only');
31
+ const concRaw = val('--concurrency');
32
+ const CONC = concRaw === null ? 6 : Number(concRaw);
33
+ if (!Number.isInteger(CONC) || CONC < 1) {
34
+ console.error(`run-tests: --concurrency 须为正整数(现 ${concRaw})`);
35
+ process.exit(1);
36
+ }
37
+
38
+ let suites;
39
+ try {
40
+ suites = fs.readdirSync(dir).filter((f) => f.endsWith('.test.mjs') && (!only || f.includes(only))).sort();
41
+ } catch (e) {
42
+ console.error(`run-tests: 无法读取目录 ${dir}(${e.code || e.message})`);
43
+ process.exit(1);
44
+ }
45
+ if (!suites.length) {
46
+ console.error(`run-tests: ${dir} 下无 *.test.mjs${only ? `(--only ${only})` : ''}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ // 工作池调度(完成一个补一个——分块会让最慢套件阻塞同块槽位,2026-10-09 实测 check-loop.test 独占 ~1/3 时长)
51
+ let totalPass = 0, totalFail = 0, red = 0;
52
+ const runSuite = (s) => new Promise((resolve) => {
53
+ const child = spawn(process.execPath, [path.join(dir, s)]);
54
+ let out = '', err = '';
55
+ child.stdout.on('data', (d) => { out += d; });
56
+ child.stderr.on('data', (d) => { err += d; });
57
+ child.on('close', (code) => resolve({ stdout: out, stderr: err, status: code, error: null }));
58
+ child.on('error', (e) => resolve({ stdout: out, stderr: err, status: null, error: e }));
59
+ });
60
+ const replay = (s, r) => {
61
+ const m = [...String(r.stdout || '').matchAll(/合计: PASS (\d+) \/ FAIL (\d+)/g)].pop();
62
+ const p = m ? Number(m[1]) : 0;
63
+ const f = m ? Number(m[2]) : 0;
64
+ totalPass += p;
65
+ totalFail += f;
66
+ const ok = r.status === 0 && f === 0;
67
+ process.stdout.write(`\n[run-tests] ▶ ${s}\n`);
68
+ if (r.stdout) process.stdout.write(r.stdout);
69
+ if (ok) {
70
+ console.log(`[run-tests] OK ${s}(PASS ${p} / FAIL ${f})`);
71
+ } else {
72
+ red++;
73
+ console.log(`[run-tests] RED ${s}${r.status != null ? `(exit ${r.status})` : r.error ? `(spawn ${r.error.code || r.error.message})` : ''}${m ? `(PASS ${p} / FAIL ${f})` : ''}`);
74
+ if (r.stderr) process.stderr.write(String(r.stderr).split('\n').slice(-5).join('\n') + '\n');
75
+ }
76
+ };
77
+ {
78
+ const queue = [...suites];
79
+ const inflight = new Map(); // suite -> promise(回放按提交序记录、完成即按序回放:用「下一个待回放」指针)
80
+ const pending = []; // {s, promise}
81
+ let cursor = 0;
82
+ const launch = () => {
83
+ while (inflight.size < CONC && queue.length) {
84
+ const s = queue.shift();
85
+ const pr = runSuite(s).then((r) => ({ s, r }));
86
+ inflight.set(s, pr);
87
+ pending.push(pr);
88
+ }
89
+ };
90
+ launch();
91
+ while (cursor < pending.length) {
92
+ const { s, r } = await pending[cursor++];
93
+ inflight.delete(s);
94
+ replay(s, r);
95
+ launch();
96
+ }
97
+ }
98
+ console.log(`\n[run-tests] 套件 ${suites.length}(红 ${red},并发 ${CONC})|断言合计 PASS ${totalPass} / FAIL ${totalFail}${red ? '——红套件见上,修复后原路重跑' : '——全绿'}`);
99
+ process.exit(red ? 1 : 0);
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // run-tests.mjs 的夹具驱动测试(2026-10-08 agents-hygiene 批2)
3
+ // 场景:①全绿目录 → exit 0 且聚合计数正确;②含红套件(FAIL>0 + exit 1)→ 记红且 exit 1;
4
+ // ③崩溃套件(throw,无合计行)→ 记红且 exit 1;④--only 过滤;⑤空目录 → exit 1。
5
+ // 用法:node .agents/scripts/run-tests.test.mjs(在仓库任意目录执行均可)
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
+ const MAIN = path.join(SCRIPT_DIR, 'run-tests.mjs');
14
+ const USAGE = '用法:node .agents/scripts/run-tests.test.mjs';
15
+
16
+ let pass = 0;
17
+ let fail = 0;
18
+ const check = (desc, cond, detail = '') => {
19
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
20
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
21
+ };
22
+
23
+ const mkfix = (files) => {
24
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'run-tests-fx-'));
25
+ for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(d, name), body);
26
+ return d;
27
+ };
28
+ const run = (args) => spawnSync(process.execPath, [MAIN, ...args], { encoding: 'utf8' });
29
+
30
+ // ---- 场景 1:全绿目录 → exit 0 且聚合计数(非 .test.mjs 文件不入围)----
31
+ {
32
+ const d = mkfix({ 'a.test.mjs': 'console.log("合计: PASS 3 / FAIL 0");', 'b.test.mjs': 'console.log("合计: PASS 2 / FAIL 0");', 'readme.md': 'x' });
33
+ const r = run(['--dir', d]);
34
+ check('场景 1:全绿目录 → exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
35
+ check('场景 1:聚合计数 PASS 5 / FAIL 0 且红 0', /断言合计 PASS 5 \/ FAIL 0/.test(r.stdout) && /红 0/.test(r.stdout), r.stdout);
36
+ fs.rmSync(d, { recursive: true, force: true });
37
+ }
38
+
39
+ // ---- 场景 2:红套件(计数 FAIL>0 且 exit 1)→ 记红且 exit 1 ----
40
+ {
41
+ const d = mkfix({ 'a.test.mjs': 'console.log("合计: PASS 3 / FAIL 0");', 'bad.test.mjs': 'console.log("合计: PASS 1 / FAIL 2"); process.exit(1);' });
42
+ const r = run(['--dir', d]);
43
+ check('场景 2:含红套件 → exit 1', r.status === 1, `exit=${r.status}`);
44
+ check('场景 2:红套件被点名', /RED bad\.test\.mjs/.test(r.stdout) && /红 1/.test(r.stdout) && /FAIL 2/.test(r.stdout), r.stdout);
45
+ fs.rmSync(d, { recursive: true, force: true });
46
+ }
47
+
48
+ // ---- 场景 3:崩溃套件(无合计行)→ 记红且 exit 1 ----
49
+ {
50
+ const d = mkfix({ 'boom.test.mjs': 'throw new Error("boom");' });
51
+ const r = run(['--dir', d]);
52
+ check('场景 3:崩溃套件 → exit 1 且标红', r.status === 1 && /RED boom\.test\.mjs(exit 1)/.test(r.stdout), r.stdout);
53
+ fs.rmSync(d, { recursive: true, force: true });
54
+ }
55
+
56
+ // ---- 场景 4:--only 过滤 ----
57
+ {
58
+ const d = mkfix({ 'aa.test.mjs': 'console.log("合计: PASS 1 / FAIL 0");', 'bb.test.mjs': 'console.log("合计: PASS 2 / FAIL 0");' });
59
+ const r = run(['--dir', d, '--only', 'bb']);
60
+ check('场景 4:--only 只跑匹配套件', r.status === 0 && r.stdout.includes('bb.test.mjs') && !r.stdout.includes('aa.test.mjs') && /套件 1/.test(r.stdout), r.stdout);
61
+ fs.rmSync(d, { recursive: true, force: true });
62
+ }
63
+
64
+ // ---- 场景 5:空目录 → exit 1 报无套件 ----
65
+ {
66
+ const d = mkfix({ 'readme.md': 'x' });
67
+ const r = run(['--dir', d]);
68
+ check('场景 5:空目录 → exit 1 报无套件', r.status === 1 && /无 \*\.test\.mjs/.test(r.stderr || ''), `${r.status}\n${r.stderr}`);
69
+ fs.rmSync(d, { recursive: true, force: true });
70
+ }
71
+
72
+ // ---- 场景 6:--concurrency 校验——非正整数 fail-loud ----
73
+ {
74
+ const d = mkfix({ 'a.test.mjs': 'console.log("合计: PASS 1 / FAIL 0");' });
75
+ for (const bad of ['0', '-2', 'abc', '2.5']) {
76
+ const r = run(['--dir', d, '--concurrency', bad]);
77
+ check(`场景 6:--concurrency ${bad} → exit 1 fail-loud`, r.status === 1 && /正整数/.test(r.stderr), `exit=${r.status}\n${r.stderr}`);
78
+ }
79
+ const ok = run(['--dir', d, '--concurrency', '1']);
80
+ check('场景 6:--concurrency 1(串行兼容)→ exit 0 聚合正确', ok.status === 0 && /断言合计 PASS 1 \/ FAIL 0/.test(ok.stdout), `exit=${ok.status}\n${ok.stdout}`);
81
+ const okN = run(['--dir', d, '--concurrency', '8']);
82
+ check('场景 6:--concurrency 8(超套件数)→ exit 0', okN.status === 0 && /断言合计 PASS 1 \/ FAIL 0/.test(okN.stdout), `exit=${okN.status}\n${okN.stdout}`);
83
+ fs.rmSync(d, { recursive: true, force: true });
84
+ }
85
+
86
+ // ---- 场景 7:并行回放完整(每套件有 ▶ 分隔头且计数聚合不分串)----
87
+ {
88
+ const d = mkfix({
89
+ 'a.test.mjs': 'console.log("a-out 合计: PASS 1 / FAIL 0");',
90
+ 'b.test.mjs': 'console.log("b-out 合计: PASS 2 / FAIL 0");',
91
+ 'c.test.mjs': 'console.log("c-out 合计: PASS 3 / FAIL 0");',
92
+ });
93
+ const r = run(['--dir', d]);
94
+ check('场景 7:并行全绿 → 聚合 PASS 6 且三套件回放齐全',
95
+ r.status === 0 && /断言合计 PASS 6 \/ FAIL 0/.test(r.stdout) && /▶ a\.test\.mjs/.test(r.stdout) && /▶ b\.test\.mjs/.test(r.stdout) && /▶ c\.test\.mjs/.test(r.stdout), `exit=${r.status}\n${r.stdout}`);
96
+ check('场景 7:并行下套件输出不串(a-out 紧邻其回放块)', /▶ a\.test\.mjs\na-out/.test(r.stdout), r.stdout);
97
+ fs.rmSync(d, { recursive: true, force: true });
98
+ }
99
+
100
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
101
+ if (fail) {
102
+ console.log(`\n${USAGE}`);
103
+ process.exit(1);
104
+ }
105
+ process.exit(0);
@@ -26,7 +26,7 @@ const ENUMS_FIXTURE = [
26
26
  'doc.status.abandoned=superseded cancelled',
27
27
  'incident.status.all=open fixed closed',
28
28
  'incident.status.active=open',
29
- 'level.all=L0 L1 L2 L3',
29
+ 'level.all=L1 L2 L3',
30
30
  ].join('\n') + '\n';
31
31
 
32
32
  const mkfix = () => {
@@ -6,15 +6,18 @@
6
6
  // incident 入口 = 文件存在 + 「时间线」小节内、行首列表条目含「用户确认」(原 maintain.md §3 约定;
7
7
  // 2026-09-30 复核 P2-1 收窄为小节内行首条目;第三轮复核 P1-1 起接受日期前缀形态,与模板样例一致);
8
8
  // 状态 ∈ {open, fixed, closed}(spec/plan 须先于 fixed 起草,故 open 即为有效态)。
9
- // 入口「级别」须 ∈ L0-L3;缺失即拒(fail-closed)——**但库存量形态短路**(`流程: legacy` 或 日期早于
9
+ // 入口「级别」须 ∈ L1-L3(2026-10-13 L0 退役,自源仓收敛系列 1 回灌;L0 → suspect 走 fail-closed);
10
+ // 缺失即拒(fail-closed)——**但库存量形态短路**(`流程: legacy` 或 日期早于
10
11
  // 确认门生效日):跳过级别校验与 spec 档(2026-09-30 增量复核 N1——存量最可能缺级别,硬拒会锁死
11
12
  // spec 起草与确认)。
12
13
  // 注意:**库存量信号本身即等于「入口已确认」**(judgeDoc:`台账行 || 存量口径`,与检查 15 / 检查 1 /
13
14
  // 配对门的既有口径一致——早于确认门生效日的自报日期即存量豁免,不要求另有台账行);此处「短路」
14
15
  // 只额外豁免**级别前置与 spec 档**,不是对台账要求的二次豁免(P2-1:原注释把二者写反,已订正)。
15
16
  // plan 门追加:入口级别(取入口 frontmatter 级别,不取工具 --level)为 L2/L3 时,同名 spec 须已确认(同口径)。
17
+ // L1 即入口例外(2026-10-13 自 Shipyard 回灌):同名 intent/incident 均无时,级别 L1 的 plan 自身即入口,
18
+ // 起草/确认门放行(文件已在则级别以文件 frontmatter 为准,禁用 --level L1 放行存量 L2 plan)。
16
19
  // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):同名 intent 仍为 draft 且风险泳道为
17
- // L0/L1(级别优先;级别缺失或非法时回落 risk_level——fill-intent 双写同值)→「先起草后确认」是协作道
20
+ // L1(级别优先;级别缺失或非法时回落 risk_level——fill-intent 双写同值)→「先起草后确认」是协作道
18
21
  // 语义(check-loop 检查 19 同款豁免口径),放行 spec/plan 起草与确认;L2/L3 防御道维持入口确认前置。
19
22
  // incident 入口不豁免(过目留痕本就轻量)。收口由 check-loop --hardening 加固门兜底(入 main 前须
20
23
  // intent approved/done + spec/plan 在场)。
@@ -25,6 +28,7 @@ import fs from 'node:fs';
25
28
  import path from 'node:path';
26
29
  import { spawnSync } from 'node:child_process';
27
30
  import { loadKitPolicy } from './policy.mjs';
31
+ import { readLedgerRows } from './ledger-loader.mjs';
28
32
 
29
33
  export const ENTRY_STATUSES = ['approved', 'done'];
30
34
  export const INCIDENT_STATUSES = ['open', 'fixed', 'closed'];
@@ -48,16 +52,10 @@ function readBody(abs) {
48
52
  }
49
53
 
50
54
  // readLedger:confirmations.jsonl 追加式台账(坏行容忍跳过;与 check-loop 检查 15 同容错口径)
55
+ // 读法收敛到 ledger-loader 单源(2026-10-12 ledger-loader-p1):本函数此前自持 readFileSync+parse
56
+ // 复刻,与其余 5 处各写各的——分片台账落地时逐处加分支必然漏改。签名与返回值不变。
51
57
  export function readLedger(root) {
52
- const p = path.join(root, '.agents', 'confirmations.jsonl');
53
- let text = '';
54
- try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
55
- const out = [];
56
- for (const line of text.split('\n')) {
57
- if (!line.trim()) continue;
58
- try { out.push(JSON.parse(line)); } catch { /* 坏行跳过 */ }
59
- }
60
- return out;
58
+ return readLedgerRows(root);
61
59
  }
62
60
 
63
61
  // judgeDoc:状态枚举 + 机器事实源判定(供入口 / spec 复用)
@@ -151,32 +149,51 @@ export function specConfirmed(root, base) {
151
149
  // suspect 消费规则(双严):红线类检查按 low 执行(拦红线);豁免类门(起草豁免/--batch/--auto/加固门
152
150
  // lane/泳道豁免/逐阶段深度)按 high 执行(不豁免)。取代原三处并存语义(任一低即低 / 级别优先回落 / 仅级别)。
153
151
  export function laneOfEntry(level, risk, riskLevelSince, date) {
154
- const L = /^L[0-3]$/.test(level || '') ? level : '';
152
+ const L = /^L[1-3]$/.test(level || '') ? level : '';
155
153
  if (!L) return 'suspect';
156
- const R = /^L[0-3]$/.test(risk || '') ? risk : '';
154
+ const R = /^L[1-3]$/.test(risk || '') ? risk : '';
157
155
  const dualProtocol = !!riskLevelSince && /^\d{4}-\d{2}-\d{2}$/.test(date || '') && date >= riskLevelSince;
158
- if (dualProtocol) return (R && R === L) ? ((L === 'L0' || L === 'L1') ? 'low' : 'high') : 'suspect';
156
+ if (dualProtocol) return (R && R === L) ? (L === 'L1' ? 'low' : 'high') : 'suspect';
159
157
  if (R && R !== L) return 'suspect';
160
- return (L === 'L0' || L === 'L1') ? 'low' : 'high';
158
+ return L === 'L1' ? 'low' : 'high';
161
159
  }
162
160
 
163
161
  // laneOfDoc:非 intent 件(spec/plan/incident——级别单字段协议):合法按值判,非法 → suspect。
164
162
  export function laneOfDoc(level) {
165
- if (!/^L[0-3]$/.test(level || '')) return 'suspect';
166
- return (level === 'L0' || level === 'L1') ? 'low' : 'high';
163
+ if (!/^L[1-3]$/.test(level || '')) return 'suspect';
164
+ return level === 'L1' ? 'low' : 'high';
167
165
  }
168
166
 
169
167
  // draftGateFor:起草门总判(kind = 'spec' | 'plan',fill 工具与 confirm-doc 逐阶段门共用)
170
- export function draftGateFor(kind, root, base) {
168
+ // opts.level(可选,2026-10-13 自 Shipyard 回灌):仅供 plan「L1 即入口」例外取级别——文件未落盘时认
169
+ // fill 工具传入的 --level;confirm-doc 调用(confirmGateFor)不传,落盘后级别一律以文件 frontmatter 为准。
170
+ export function draftGateFor(kind, root, base, opts = {}) {
171
171
  const entry = entryConfirmed(root, base);
172
172
  if (!entry.ok) {
173
- // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent 放行起草
173
+ // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):L1 draft intent 放行起草
174
174
  // ——「先动手后确认」;泳道取法自 2026-10-02 caliber-convergence 起单源 laneOfEntry(方案 C 分歧
175
175
  // 双严:suspect 不豁免——级别/risk_level 缺失、非法或分歧均按防御道走入口确认)。
176
176
  const lane = laneOfEntry(entry.level, entry.risk, loadKitPolicy(root).riskLevelSince, entry.date);
177
177
  if (entry.kind === 'intent' && entry.status === 'draft' && lane === 'low') {
178
178
  return { ok: true, entry, laneBypass: true };
179
179
  }
180
+ // L1 plan 即入口(2026-10-13 自 Shipyard 回灌):没有 intent/incident 时,这份 plan 自己就是入口。
181
+ // 文件已在则级别以文件为准(禁止用 --level L1 把已有 L2 plan 放行);尚未落盘才认 opts.level。
182
+ if (kind === 'plan' && entry.kind === null) {
183
+ const planAbs = path.join(root, 'workflow', 'plans', `${base}.md`);
184
+ const declared = fs.existsSync(planAbs) ? readFm(planAbs, '级别') : (opts.level || '');
185
+ if (declared === 'L1') {
186
+ return {
187
+ ok: true,
188
+ entry: {
189
+ ok: true, kind: 'plan', rel: `workflow/plans/${base}.md`, level: 'L1',
190
+ status: fs.existsSync(planAbs) ? (readFm(planAbs, '状态') || 'draft') : 'draft',
191
+ selfEntry: true, legacy: false,
192
+ },
193
+ laneBypass: true,
194
+ };
195
+ }
196
+ }
180
197
  return {
181
198
  ok: false,
182
199
  reason: `同主题入口未确认——${entry.reason}`,
@@ -188,11 +205,11 @@ export function draftGateFor(kind, root, base) {
188
205
  // spec 起草与确认。注意:库存量信号**本身即等于入口已确认**(judgeDoc 的 `台账行 || 存量口径`,
189
206
  // 与检查 1 / 15 / 配对门同口径)——台账豁免发生在 judgeDoc,不在本处;本处不做二次豁免
190
207
  // (第三轮复核 P2-1:原句「台账要求不豁免」把两件事写反,已订正)。
191
- if (!entry.storageLegacy && !/^L[0-3]$/.test(entry.level)) {
208
+ if (!entry.storageLegacy && !/^L[1-3]$/.test(entry.level)) {
192
209
  return {
193
210
  ok: false,
194
211
  reason: `入口缺合法「级别」字段(现「${entry.level || '缺失'}」,无法判定前置深度)`,
195
- hint: '先给入口补「级别: L0-L3」字段,再继续',
212
+ hint: '先给入口补「级别: L1-L3」字段,再继续',
196
213
  };
197
214
  }
198
215
  if (kind === 'plan' && !entry.storageLegacy && (entry.level === 'L2' || entry.level === 'L3')) {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // trust-mode: AI 自治信任等级管理(2026-09-30 ai-autonomy-trust;同日 hybrid-governance-risk-lanes 加名称双轨)
3
- // 管理 .agents/trust-mode.json,授权 AI 对 L0/L1 文档进行自治放行(--auto)。
3
+ // 管理 .agents/trust-mode.json,授权 AI 对 L1 文档进行自治放行(--auto)。
4
4
  // 等级语义(名称 ↔ 数值双轨:confirm-doc.mjs 读数值 enabled/level,人机接口名称与数值皆收):
5
5
  // Strict (level 0, enabled=false): AI 自治关闭(默认)——confirm-doc --auto 一律拒绝
6
- // Standard (level 1): AI 可自批 L0/L1 draft -> approved
7
- // Trusted (level 2): AI 可自批 L0/L1 draft -> approved 及 approved -> done(全闭环)
6
+ // Standard (level 1): AI 可自批 L1 draft -> approved
7
+ // Trusted (level 2): AI 可自批 L1 draft -> approved 及 approved -> done(全闭环)
8
8
  // 安全默认值:未开启(enabled: false)时,confirm-doc.mjs --auto 一律拒绝。
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
@@ -45,7 +45,7 @@ function main() {
45
45
  }
46
46
  writeConfig({ enabled: true, level, name: NAME_BY_LEVEL[level], enabledAt: new Date().toISOString() });
47
47
  console.log(`✅ AI 自治模式已开启 (${NAME_BY_LEVEL[level]} / Level ${level})`);
48
- console.log(level === 1 ? ' 授权范围:L0/L1 文档 draft -> approved' : ' 授权范围:L0/L1 文档全闭环 (approved + done)');
48
+ console.log(level === 1 ? ' 授权范围:L1 文档 draft -> approved' : ' 授权范围:L1 文档全闭环 (approved + done)');
49
49
  return;
50
50
  }
51
51
 
@@ -30,7 +30,7 @@ const mkfix = () => {
30
30
  fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'),
31
31
  ['doc.status.all=draft approved done superseded cancelled', 'doc.status.confirmed=approved done superseded cancelled',
32
32
  'doc.status.active=draft approved', 'doc.status.terminal=done superseded cancelled', 'doc.status.abandoned=superseded cancelled',
33
- 'incident.status.all=open fixed closed', 'incident.status.active=open', 'level.all=L0 L1 L2 L3'].join('\n') + '\n');
33
+ 'incident.status.all=open fixed closed', 'incident.status.active=open', 'level.all=L1 L2 L3'].join('\n') + '\n');
34
34
  fs.writeFileSync(path.join(d, '.agents', 'workflow-modules.txt'), '# modules\npipeline\nui\n');
35
35
  return d;
36
36
  };
@@ -1,38 +1,63 @@
1
1
  #!/usr/bin/env node
2
- // verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script;2026-09-26 check-loop-node 第 2 步直连 node)
3
- // 用途:关单前一键过门——按写死顺序执行 npm test → check-loop,逐项输出 pass/fail。
4
- // 语义:只编排不裁决——两条命令都是既有门禁原样跑,本脚本不读配置、不判窗口、不改退出码;
5
- // 任一步非零即 fail-fast 退出 1,失败原因原样透传不吞。
6
- // check-loop 为 node 实现(经 check-loop.mjs 直跑——node 即本脚本运行时必然可用,
7
- // 旧「缺 sh 环境 fail-closed」分支随 sh 依赖消失退役)。
8
- // 凭证落账(2026-10-07 verify-evidence):两步全绿后向 <root>/.agents/verifications.jsonl append
9
- // 一行机器事实 {ts, exitCode:0, suite, passed?, failed?, runId?}——append-only 事实记录,不属裁决
10
- // (窗口判定/门禁在消费方 confirm-doc 与 check-loop 检查 8);消费语义 = 「按门跑了 verify 且全绿」,
11
- // 裸跑 npm test 不落账(对齐引导)。npm test 步骤输出从 inherit 改 pipe 捕获后回放(延迟显示,
12
- // 关单场景可接受)——仅 best-effort 抓编排器权威汇总行 `[run-tests] 合计: SUITES n / FAILED m`,
13
- // 抓不到则省略计数键(宁缺勿错:exitCode 才是硬事实,计数缺失不阻断)。
14
- // 装户仓(根目录无 package.json,工程在子目录)下默认 npm test 恒 ENOENT(exit 4058)→ **显式跳过步骤 1**
15
- // 且**不落测试绿凭证**(凭证语义 = 测试真跑绿,跳过即无凭证,防假绿),并提示用
16
- // --test-cmd "<项目测试命令>" 指定;显式 --test-cmd 不受此判定影响(2026-10-07 papercut)。
17
- // 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
18
- // --doc 凭证绑定(2026-10-08 verify-doc-binding):落账行增 `doc` 字段,写入**相对仓库根的 posix 形态**
19
- // (win32 反斜杠 / posix 斜杠 / 绝对路径三种写法经 resolve+relative+分隔符归一后落同一值),使凭证
20
- // 与被证明的那一单绑定。不带 --doc 时仍落行但**不写该键**——CI(kit-ci.yml)无单次上下文,照常记录
21
- // 事实;这样的行不给任何单背书(消费方按 doc 精确匹配时 e.doc 为 undefined 恒不命中)。
22
- // 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"] [--doc <本单 workflow 路径>]
23
- // (--test-cmd 用于测试注入,或在无根 package.json 的装户仓指定项目测试命令;
24
- // --doc 把凭证绑到本单,如 --doc workflow/intents/<日期>-<主题>.md)
2
+ // verify.mjs — 关单/CI 统一静态门编排(GATES 表版;2026-10-13 自 Shipyard.Material 参考实现回灌,
3
+ // 源仓沿革留痕在包源仓 git 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 用途:关单或 CI 前一键过门——engine cohort 七门按表执行、逐项输出 pass/fail,任一门红即 fail-fast exit 1。
5
+ // 语义:单源口径。本地 verify 与装户远端门(kit-ci.yml 同一条命令)行为 100% 一致。
6
+ // 凭证落账:全绿才落绿凭证;任一门红即落失败行(suite=门 id),供根因分析——消费方(confirm-doc 前置 /
7
+ // check-loop 检查 8)只认 exitCode===0 的行,失败行零污染凭证判定。
8
+ // 七门照搬源仓 engine cohort(不搬 product cohort——frontend/backend 门是源仓项目私有;lane-surface-ci 门
9
+ // 依赖 kit 侧 --ci 支持核实,留后续批次评估)。源仓 --engine 增量语义不移植:本表已全为引擎段,
10
+ // 「部分绿 ≠ 全量凭证」的分野不存在。
11
+ // kit 特有语义(沿现口径保留):
12
+ // a) 装户仓判定——cwd 无 package.json → engine-tests 门显式跳过(跳过≠通过,不计入门数)且**不落
13
+ // 测试绿凭证**(凭证语义 = 测试真跑绿,防假绿),提示 --test-cmd;
14
+ // b) 凭证行形状 = {ts, exitCode:0, suite:'npm test', gates, headShort?, passed?, failed?, doc?, runId?}
15
+ // (gates/headShort 自源仓回灌新增——非 git 环境解析失败则省略、不伪造;消费方只读 ts/exitCode/doc,
16
+ // 增量字段零影响);计数捕获只认编排器权威行 `[run-tests] 合计: SUITES n / FAILED m`
17
+ // (嵌套套件同形行不回退——宁缺勿错,exitCode 才是硬事实);
18
+ // c) --doc 绑定语义保留(归一基准 = ROOT,与 confirm-doc / 检查 8 三处同源);
19
+ // d) --test-cmd 为 fixture 测试注入缝:替换全部生产门为 [test, check-loop] 两门夹具(夹具根里没有
20
+ // 脚本本体,check-loop 用绝对路径直调,root 归属仍由 CHECK_LOOP_ROOT 裁决);夹具模式不触发生效规则
21
+ // 视图门与摘要生成(假门凭证只进本地台账,不产 committed 摘要)。
22
+ // 未执行门清单:fail-fast 后序门状态未知≠通过——验证系统输出须完备(「没跑」与「过了」可区分)。
23
+ // 生效规则视图门(effective-policy --check):policy 变更后必须重生成 workflow/effective-policy.md 随同
24
+ // 提交——新装户由 init 生成器首跑种子该文件,缺文件对 --check 是漂移(红),不静默放行。
25
+ // 绿凭证摘要(gen-verify-evidence.mjs):全绿尾部自动重生成 workflow/verify-evidence.md——全绿事实与
26
+ // 仓内摘要同刻生成,消除「本地台账已绿、committed 摘要滞后」的双真相分叉;生成失败响亮告警但不推翻绿裁决。
27
+ // 用法:node .agents/scripts/verify.mjs [--list] [--test-cmd "<shell 命令>"] [--doc <本单 workflow 路径>]
28
+ // --list:按执行序打印门 id 后退出,不执行(与 --test-cmd 互斥)。
29
+ // --test-cmd 用于测试注入,或在无根 package.json 的装户仓指定项目测试命令;
30
+ // --doc 把凭证绑到本单,如 --doc workflow/intents/<日期>-<主题>.md
31
+ // 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具)
25
32
  import fs from 'node:fs';
26
33
  import { spawnSync } from 'node:child_process';
27
34
  import path from 'node:path';
28
35
  import { fileURLToPath } from 'node:url';
29
36
 
30
37
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
38
+ const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
39
+
40
+ // --- 门禁定义(engine cohort 七门,序即执行序;遇红即停) ---
41
+ const GATES = [
42
+ { id: 'scripts-syntax', name: '引擎脚本语法自检', cmd: 'node .agents/scripts/check-scripts-syntax.mjs', folder: '.', type: 'shell' },
43
+ { id: 'engine-tests', name: '测试门 (npm test)', cmd: 'npm test', folder: '.', type: 'shell' },
44
+ { id: 'workflows-check', name: '编排脚本检查', cmd: 'node .agents/scripts/workflows-check.mjs', folder: '.', type: 'shell' },
45
+ { id: 'rule-budget', name: '常驻面预算检查', cmd: 'node .agents/scripts/rule-budget.mjs --all', folder: '.', type: 'shell' },
46
+ { id: 'effective-policy', name: '生效规则视图漂移检查', cmd: 'node .agents/scripts/gen-effective-policy.mjs --check', folder: '.', type: 'shell' },
47
+ // 默认硬门。卫生段不进关单(2026-10-13 B6b-1 分道随源仓口径):要看时另跑 check-loop.mjs --hygiene。
48
+ { id: 'check-loop', name: '闭环门禁校验', cmd: 'node .agents/scripts/check-loop.mjs', folder: '.', type: 'shell' },
49
+ { id: 'ledger-invariant', name: '确认台账不变量(历史全扫)', cmd: 'node .agents/scripts/check-ledger-invariant.mjs', folder: '.', type: 'shell' },
50
+ ];
51
+
52
+ // fixture 夹具门(--test-cmd 注入时替换全部生产门,verify.test.mjs 断言依赖此口径)
53
+ // check-loop 用绝对路径直调真实脚本——夹具根里没有脚本本体,root 归属仍由 CHECK_LOOP_ROOT 裁决。
54
+ const fixtureGates = (cmd) => [
55
+ { id: 'test', name: '测试步(注入)', cmd, folder: '.', type: 'shell' },
56
+ { id: 'check-loop', name: '闭环门禁校验', cmd: `node "${path.join(SCRIPT_DIR, 'check-loop.mjs')}"`, folder: '.', type: 'shell' },
57
+ ];
58
+
31
59
  const args = process.argv.slice(2);
32
60
  const explicitTestCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : null;
33
- const testCmd = explicitTestCmd ?? 'npm test';
34
- const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
35
- const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
36
61
 
37
62
  // --doc 解析(沿 --test-cmd 同款取下一 argv 的写法;缺值报用法错误 exit 1 与 --test-cmd 对称)。
38
63
  // 归一基准 = **ROOT**(不是 cwd)——消费方两处都以仓库根为基准(check-loop 检查 8 用
@@ -50,50 +75,89 @@ const docRel = docArg
50
75
  ? path.relative(ROOT, path.resolve(ROOT, docArg)).split(path.sep).join('/')
51
76
  : null;
52
77
 
53
- // 装户仓判定:默认命令只在**当前目录**有 package.json 时才跑(npm test 亦在该目录执行)。
54
- // 跳过时后续不落测试绿凭证(见文末落账段)。
78
+ // 装户仓判定:engine-tests 门只在**当前目录**有 package.json 时才跑(npm test 亦在该目录执行)。
79
+ // 跳过时后续不落测试绿凭证(见全绿落账段)。显式 --test-cmd 不受此判定影响。
55
80
  const hasRootPackageJson = fs.existsSync(path.join(process.cwd(), 'package.json'));
56
81
  const skipTest = explicitTestCmd === null && !hasRootPackageJson;
57
- let testStdout = '';
58
-
59
- if (skipTest) {
60
- console.log('[verify] ⏭ 1/2 跳过 npm test:当前目录无 package.json(装户仓)——请按项目口径手工跑测试,或用 --test-cmd "<项目测试命令>" 指定后重跑');
61
- } else {
62
- console.log(`[verify] ▶ 1/2 ${testCmd}`);
63
- const t = spawnSync(testCmd, { shell: true, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
64
- if (t.stdout) process.stdout.write(t.stdout);
65
- if (t.stderr) process.stderr.write(t.stderr);
66
- if (t.status !== 0) {
67
- console.error(`[verify] ❌ 1/2 ${testCmd} 未通过(exit ${t.status ?? t.error?.code})——修复后重跑,非绿不关单`);
82
+ const gates = explicitTestCmd ? fixtureGates(explicitTestCmd) : GATES;
83
+
84
+ if (args.includes('--list')) {
85
+ if (explicitTestCmd) {
86
+ console.error('[verify] --list 与 --test-cmd 互斥');
68
87
  process.exit(1);
69
88
  }
70
- testStdout = String(t.stdout || '');
71
- console.log('[verify] ✅ 1/2 测试通过');
89
+ for (const g of gates) console.log(g.id);
90
+ process.exit(0);
72
91
  }
73
92
 
74
- console.log('[verify] ▶ 2/2 check-loop');
75
- const c = spawnSync(process.execPath, [checkLoop], { stdio: 'inherit' });
76
- if (c.status !== 0) {
77
- console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——按上方输出修复后原路重跑`);
78
- process.exit(1);
93
+ // 失败留痕:失败行 {ts, exitCode, suite:门 id, note}——append-only 事实记录,供根因分析;
94
+ // 消费方只认 exitCode===0 的绿行,失败行零污染凭证判定。IO 失败出账不阻断。
95
+ const recordFailure = (step, exitInfo, note) => {
96
+ try {
97
+ const entry = { ts: new Date().toISOString(), exitCode: typeof exitInfo === 'number' ? exitInfo : -1, suite: step, note };
98
+ if (process.env.PIPELINE_RUN_ID) entry.runId = process.env.PIPELINE_RUN_ID;
99
+ fs.mkdirSync(path.join(ROOT, '.agents'), { recursive: true });
100
+ fs.appendFileSync(path.join(ROOT, '.agents', 'verifications.jsonl'), `${JSON.stringify(entry)}\n`);
101
+ console.log(`[verify] 🧾 失败行已留痕 verifications.jsonl(suite=${step})`);
102
+ } catch (e) {
103
+ console.error(`[verify] ⚠️ 失败行落账失败(${e.code || e.message})`);
104
+ }
105
+ };
106
+
107
+ console.log(`[verify] 🚀 开始静态门验证 (共 ${gates.length} 项)${explicitTestCmd ? '【fixture 模式】' : ''}\n`);
108
+
109
+ let passedCount = 0;
110
+ const outputs = {};
111
+
112
+ for (let i = 0; i < gates.length; i++) {
113
+ const gate = gates[i];
114
+ // 装户仓:engine-tests 门显式跳过——跳过≠通过,不计入 passedCount、不算绿(全绿落账段据此不落凭证)
115
+ if (skipTest && gate.id === 'engine-tests') {
116
+ console.log(`[verify] ⏭ ${i + 1}/${gates.length} 跳过 ${gate.cmd}:当前目录无 package.json(装户仓)——请按项目口径手工跑测试,或用 --test-cmd "<项目测试命令>" 指定后重跑`);
117
+ continue;
118
+ }
119
+ console.log(`[verify] ▶ ${i + 1}/${gates.length} ${gate.name}...`);
120
+
121
+ const result = spawnSync(gate.cmd, {
122
+ shell: true,
123
+ encoding: 'utf8',
124
+ cwd: path.join(ROOT, gate.folder),
125
+ maxBuffer: 32 * 1024 * 1024,
126
+ });
127
+
128
+ if (result.stdout) process.stdout.write(result.stdout);
129
+ if (result.stderr) process.stderr.write(result.stderr);
130
+
131
+ if (result.status !== 0) {
132
+ const note = `exit ${result.status ?? result.error?.code}`;
133
+ console.error(`[verify] ❌ ${i + 1}/${gates.length} ${gate.name} 未通过 (${note})`);
134
+ // 未执行门清单:fail-fast 后序门状态未知≠通过——「没跑」在台账里与「过了」不可区分即输出不完备
135
+ const remaining = gates.slice(i + 1).map((g) => g.id);
136
+ if (remaining.length) console.error(`[verify] ⏭️ 未执行门(fail-fast,状态未知):${remaining.join('、')}`);
137
+ recordFailure(gate.id, result.status, note);
138
+ process.exit(1);
139
+ }
140
+ outputs[gate.id] = result.stdout || '';
141
+ console.log(`[verify] ✅ ${i + 1}/${gates.length} ${gate.name} 通过\n`);
142
+ passedCount++;
79
143
  }
80
- console.log('[verify] ✅ 2/2 闭环校验通过');
81
144
 
82
- // 凭证落账:两步全绿才落(check-loop 红不产生凭证——凭证语义 = verify 全绿);**步骤 1 被跳过同样不落**
145
+ // 凭证落账:全部门绿才落(任一门红已在上面 fail-fast 退出);**测试门被跳过同样不落**
83
146
  // (测试未跑即无「测试绿」事实,落账即假绿);IO 失败出账不阻断
84
147
  try {
85
148
  if (skipTest) {
86
- console.log('[verify] ⚠️ 步骤 1 已跳过——本次不落测试绿凭证(凭证语义 = 测试真跑绿)');
149
+ console.log('[verify] ⚠️ 测试门已跳过——本次不落测试绿凭证(凭证语义 = 测试真跑绿)');
87
150
  } else {
88
- // 计数行**只认编排器权威前缀**(2026-10-09 审查 P0-2):此前是 pop 通配「合计: PASS n / FAIL n」,
89
- // 而全仓 45+ 套件每个都打印自己那行同形文本 → 取到的是字母序最后一个套件的场景数
90
- // (本仓实况:workflows-check.test.mjs 的 15),落进「一行机器事实」的 passed 字段。
91
- // 改为精确匹配 run-tests.mjs 的专属行 + 套件口径:嵌套套件同形行不再被误取,
92
- // 非本编排器口径的 --test-cmd(装户项目自有测试命令)抓不到即省略计数键,不回退到错的事实。
93
- const m = [...testStdout.matchAll(/\[run-tests\] 合计: SUITES (\d+) \/ FAILED (\d+)/g)].pop();
94
151
  const entry = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
95
- // passed/failed 口径 = **套件**(非断言、非场景):凭证明此记录的是「跑过多少套、挂了几套」。
96
- // 步骤 1 红已在上面 fail-fast 退出,故落账行 failed 恒为 0。
152
+ // 门数:凭证明此记录「过全量门禁全绿」是几门(fixture 模式 = 2 门夹具,仍如实记)
153
+ entry.gates = passedCount;
154
+ // HEAD 短 SHA 钉进绿行本体(源仓回灌):确定性证据此前只存在于测试夹具、绿行无 SHA 可锚定验证对象;
155
+ // 非 git 环境(fixture 根在 os.tmpdir)解析失败则省略该键,不伪造。
156
+ const rev = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8', cwd: ROOT });
157
+ if (rev.status === 0 && rev.stdout) entry.headShort = rev.stdout.trim();
158
+ // 计数行**只认编排器权威前缀**:嵌套套件同形行不回退——非本编排器口径的 --test-cmd
159
+ //(装户项目自有测试命令)抓不到即省略计数键,不回退到错的事实(宁缺勿错,exitCode 才是硬事实)。
160
+ const m = [...(outputs['engine-tests'] || outputs['test'] || '').matchAll(/\[run-tests\] 合计: SUITES (\d+) \/ FAILED (\d+)/g)].pop();
97
161
  if (m) { entry.passed = Number(m[1]); entry.failed = Number(m[2]); }
98
162
  if (process.env.PIPELINE_RUN_ID) entry.runId = process.env.PIPELINE_RUN_ID;
99
163
  // doc 键:未传 --doc 时**不写该键**(不是写 null)——消费方按 doc 精确匹配,undefined 与 null 都恒不命中,
@@ -106,4 +170,17 @@ try {
106
170
  } catch (e) {
107
171
  console.error(`[verify] ⚠️ 凭证落账失败(${e.code || e.message})——不阻断,关单门前置将按无凭证告警`);
108
172
  }
109
- console.log('[verify] ✅ 全绿——可以关单(逐条勾验验收标准补证据后 intent/plan → done)');
173
+
174
+ // 摘要事件原子化:全绿事实与仓内摘要(workflow/verify-evidence.md)同刻生成,消除「本地台账已绿、
175
+ // committed 摘要滞后」的双真相分叉。仅生产模式(fixture 假门凭证不产 committed 摘要;测试门跳过时
176
+ // 本就无凭证);生成失败响亮告警但不推翻绿裁决(同落账口径)。
177
+ if (!explicitTestCmd && !skipTest) {
178
+ const gen = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-verify-evidence.mjs'), '--root', ROOT], { encoding: 'utf8' });
179
+ if (gen.status === 0) {
180
+ if (gen.stdout) process.stdout.write(String(gen.stdout).trimEnd() + '\n');
181
+ } else {
182
+ console.error(`[verify] ⚠️ 绿凭证摘要生成失败(${String(gen.stderr || gen.stdout || '').trim().slice(0, 200)})——绿裁决不变,手动补跑 node .agents/scripts/gen-verify-evidence.mjs`);
183
+ }
184
+ }
185
+
186
+ console.log(`\n[verify] 🎉 全绿——可以关单(共 ${passedCount} 项通过;逐条勾验验收标准补证据后 intent/plan → done)`);