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,262 @@
1
+ // flow-kit route / run —— 手动路由与自动流水线(2026-10-13 flow-kit-route)
2
+ //
3
+ // 这是引擎从「骨架」到「执行器」的两条新命令。分工(硬约定):
4
+ // **代码留在装户 .agents/scripts/**,包 CLI 只做**薄壳转发与判定面**——执行体不搬家(决策 A)。
5
+ // **md 指令与 md 子代理仍装在装户**(.agents/commands、.agents/roles、modules/hosts):宿主读它们,
6
+ // 包 CLI 不复制、不改写、不解释它们的正文。
7
+ //
8
+ // ── route(手动路由的机器面)──────────────────────────────────────────
9
+ // 此前定级只以散文存在于 .agents/commands/new-task.md;AI 每次判级都可能不同,门禁无从校验判低守约。
10
+ // 本命令把「能被机器判定的部分」抽出来(判据单源 = .agents/scripts/level-criteria.mjs),
11
+ // 给出:建议档位 + 判据清单 + 下一阶段 + 该读的 md 指令路径。
12
+ // **诚实边界**:级别是**意图面**的语义判定(「一处定义、多处消费」),机器判不了语义——本命令只兜
13
+ // 「就高不就低」与「路径下限」(.agents/level-hints.txt,装户自持、内容项目私有)。
14
+ // 未配置 hints 时按 L1 起步并提示补配置,**绝不假装判得出**。
15
+ //
16
+ // ── run(自动流水线薄壳)─────────────────────────────────────────────
17
+ // 直接转发装户的 pipeline-run 状态机(start/next/status/watch/abort),不复制其逻辑:
18
+ // 状态机唯一实现在 .agents/scripts/pipeline-run.mjs,工单协议与人件门语义见 .agents/commands/pipeline-run.md。
19
+ // 薄壳只做:定位装户脚本 → 透传参数与退出码。**绝不 spawn AI 会话、永不伪造原话、永不带 --no-verify**。
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { spawnSync } from 'node:child_process';
23
+ import { pathToFileURL } from 'node:url';
24
+
25
+ const AGENT_SCRIPTS_REL = '.agents/scripts';
26
+
27
+ function parseArgs(args) {
28
+ const o = { _: [] };
29
+ for (let i = 0; i < args.length; i++) {
30
+ const a = args[i];
31
+ if (a.startsWith('--')) {
32
+ // `--k=v` 与 `--k v` 等价(--dir=<path> 这类写法必须能解析,否则寻址参数被静默忽略)
33
+ const eq = a.indexOf('=');
34
+ if (eq > 2) { o[a.slice(2, eq)] = a.slice(eq + 1); continue; }
35
+ const k = a.slice(2);
36
+ const next = args[i + 1];
37
+ if (next === undefined || next.startsWith('--')) o[k] = true;
38
+ else { o[k] = next; i++; }
39
+ } else o._.push(a);
40
+ }
41
+ return o;
42
+ }
43
+
44
+ function resolveTarget(o) {
45
+ return path.resolve(typeof o.dir === 'string' ? o.dir : process.cwd());
46
+ }
47
+
48
+ function fail(msg, code = 1) { console.error('❌ ' + msg); process.exit(code); }
49
+
50
+ // 装户脚本存在性判定(薄壳不做「自动 sync 装一遍」——那会静默改用户盘面,fail-loud 指向处置动作)
51
+ function requireInstalledScript(target, rel) {
52
+ const abs = path.join(target, ...rel.split('/'));
53
+ if (!fs.existsSync(abs)) {
54
+ fail(`未找到 ${rel}——当前目录不是已安装 flow-kit 的项目,或安装被部分清理。\n 处置:cd 到项目根,或执行 flow-kit init / flow-kit sync 补齐。`);
55
+ }
56
+ return abs;
57
+ }
58
+
59
+ // 读装户权威路由表(B5,2026-10-13):`new-task.md` §3 有一行机器可读注释
60
+ // <!-- route-kinds: require=plan.md | fix=maintain.md | ... -->
61
+ // 包 CLI **不再自带一份映射**——此前 STAGE_ENTRY 硬编码在包里,装户改入口文件名而不动包,
62
+ // route 就会把 AI 指向一个不存在的 md(双源漂移)。权威表就在宿主 AI 也在读的 md 里,一处即两处用。
63
+ // 读不到 → 回落到包内 DEFAULT_ROUTE_TABLE 并登记降级(老版本装户无该行,不静默假装取到单源)。
64
+ const DEFAULT_ROUTE_TABLE = {
65
+ require: 'plan.md', fix: 'maintain.md', verify: 'test.md',
66
+ review: 'review.md', deploy: 'deploy.md',
67
+ };
68
+ const ROUTE_TABLE_LINE = /<!--\s*route-kinds:\s*([^>]*?)-->/;
69
+
70
+ // route-kinds 行的**唯一解析器**——两个消费方共用:装户面 loadRouteTable(判定该读哪个 md)与
71
+ // 包源 help 的 routeKindList(列 kind 值域)。此前 help 自带第二份 kind 列表,装户改 kind 时
72
+ // help 与判定面会漂移(与 GATE_LIST 同纪律:列表一律从权威面派生,不手写第二份)。
73
+ function parseRouteTable(mdAbs) {
74
+ const m = fs.readFileSync(mdAbs, 'utf8').match(ROUTE_TABLE_LINE);
75
+ const table = {};
76
+ for (const part of String(m ? m[1] : '').split('|')) {
77
+ const kv = part.trim().match(/^([a-z]+)\s*=\s*([\w.-]+\.md)$/);
78
+ if (kv) table[kv[1]] = kv[2];
79
+ }
80
+ if (!Object.keys(table).length) throw new Error('route-kinds 行未解析出任何 kind');
81
+ return table;
82
+ }
83
+
84
+ function loadRouteTable(target, degradations) {
85
+ const p = path.join(target, '.agents', 'commands', 'new-task.md');
86
+ try {
87
+ return { table: parseRouteTable(p), source: 'new-task.md' };
88
+ } catch (e) {
89
+ degradations.push({ code: 'route-table-fallback', detail: `读不到装户 .agents/commands/new-task.md 的 route-kinds 机器单源(${String(e?.message || e).split('\n')[0]})——kind→入口回落包内默认表,装户升级 flow-kit sync 后生效` });
90
+ return { table: { ...DEFAULT_ROUTE_TABLE }, source: 'fallback' };
91
+ }
92
+ }
93
+
94
+ // 包源 help 的 kind 值域:读包源权威 md(templates/_agents/commands/new-task.md)的 route-kinds 行,
95
+ // 与装户面 loadRouteTable 共用同一解析器。读不到(包源缺该行/文件)回落包内默认表,不阻断 help。
96
+ export function routeKindList(pkgRoot) {
97
+ try {
98
+ return Object.keys(parseRouteTable(path.join(pkgRoot, 'templates', '_agents', 'commands', 'new-task.md')));
99
+ } catch {
100
+ return Object.keys(DEFAULT_ROUTE_TABLE);
101
+ }
102
+ }
103
+
104
+ // laneOfDoc:调装户 stage-gates.mjs 的单源泳道取法。返回 { key, source, reason }
105
+ // source='stage-gates.mjs' → 真取到单源;source='fallback' → 装户缺该脚本或取不到,按档位直接回落
106
+ // (fail-soft,不阻断路由)。**reason 区分「文件不存在」与「存在但 import 失败」**(2026-10-13
107
+ // executor-review)——两者曾被合并成一句「缺失」,实测把「装户脚本依赖缺失导致 import 报错」诊断成
108
+ // 「未安装」,排障方向直接被带偏。
109
+ async function laneOfDoc(target, level) {
110
+ const p = path.join(target, ...`${AGENT_SCRIPTS_REL}/stage-gates.mjs`.split('/'));
111
+ if (!fs.existsSync(p)) {
112
+ return { key: fallbackLane(level), source: 'fallback', reason: '装户 .agents/scripts/stage-gates.mjs 不存在(未安装或版本更早)' };
113
+ }
114
+ try {
115
+ const mod = await import(pathToFileURL(p).href);
116
+ if (typeof mod.laneOfDoc === 'function') {
117
+ const got = mod.laneOfDoc(level);
118
+ if (got === 'low' || got === 'high' || got === 'suspect') return { key: got, source: 'stage-gates.mjs', reason: '' };
119
+ return { key: fallbackLane(level), source: 'fallback', reason: `装户 stage-gates.mjs 未返回合法泳道值(返回 ${JSON.stringify(got)})` };
120
+ }
121
+ return { key: fallbackLane(level), source: 'fallback', reason: '装户 stage-gates.mjs 未导出 laneOfDoc(版本不匹配)' };
122
+ } catch (e) {
123
+ // 注意:这里必须把真实原因带出去——「脚本在但装不起来」与「脚本不在」是两回事。
124
+ const why = String(e?.message || e).split('\n')[0];
125
+ return { key: fallbackLane(level), source: 'fallback', reason: `装户 stage-gates.mjs 存在但 import 失败:${why}` };
126
+ }
127
+ }
128
+
129
+ const fallbackLane = (level) => ((rankOfLevel(level)) >= 2 ? 'high' : 'low');
130
+ const rankOfLevel = (level) => ({ L0: 0, L1: 1, L2: 2, L3: 3 }[String(level || '').toUpperCase()] ?? 0);
131
+
132
+ export async function route(args) {
133
+ const o = parseArgs(args);
134
+ const target = resolveTarget(o);
135
+ const scriptsDir = path.join(target, ...AGENT_SCRIPTS_REL.split('/'));
136
+ // 降级可见通道(2026-10-13 degradation-channel):此前同一命令里混用四种「缺失」策略——
137
+ // 判据单源缺 → fail-loud;stage-gates 缺 → 静默回落泳道;enums 缺 → 静默用自带表;
138
+ // hints 缺 → 静默按 L1 起步。**只有 laneSource 一个字段对外可见**,--json 消费方拿到的是同一份静默。
139
+ // 口径:fail-loud 的判据单源缺失仍在最前面(改配置比没配置危险),此后**所有软回落一律登记进
140
+ // degradations**,文本与 --json 走同一个通道——降级必须可见,但不因此阻断定级(路由不该因为
141
+ // 词表缺失整个挂掉)。
142
+ const degradations = [];
143
+
144
+ // 判据单源在装户面(随模板下发、随 sync 升级)——包不复制判据,避免双源漂移
145
+ const critPath = path.join(scriptsDir, 'level-criteria.mjs');
146
+ if (!fs.existsSync(critPath)) {
147
+ fail(`未找到 .agents/scripts/level-criteria.mjs(级别判据单源)。\n 该文件随 1.7.15+ 下发;若你的项目更早,执行 flow-kit sync 升级。`);
148
+ }
149
+ const crit = await import(pathToFileURL(critPath).href);
150
+
151
+ // 枚举单源(模块值域 / kind 集合):同样在装户面
152
+ let levelList = crit.LEVELS;
153
+ let modules = [];
154
+ try {
155
+ const enumsMod = await import(pathToFileURL(path.join(scriptsDir, 'workflow-enums.mjs')).href);
156
+ const enums = enumsMod.loadEnums(path.join(scriptsDir, '..', 'workflow-enums.txt'));
157
+ if (Array.isArray(enums['level.all']) && enums['level.all'].length) levelList = enums['level.all'];
158
+ else degradations.push({ code: 'enums-missing-key', detail: '.agents/workflow-enums.txt 无 level.all 键——档位值域回落判据单源自带表' });
159
+ } catch (e) {
160
+ degradations.push({ code: 'enums-fallback', detail: `读不到 .agents/workflow-enums.txt(${String(e?.message || e).split('\n')[0]})——档位值域回落判据单源自带表,不阻断定级` });
161
+ }
162
+ const modFile = path.join(target, '.agents', 'workflow-modules.txt');
163
+ if (fs.existsSync(modFile)) {
164
+ modules = fs.readFileSync(modFile, 'utf8').split(/\r?\n/)
165
+ .map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
166
+ }
167
+
168
+ // 路由表从装户权威 md 读(B5):kind 闭集随之以装户为准,包内不再硬编码 kind 列表
169
+ const route = loadRouteTable(target, degradations);
170
+ const kind = typeof o.kind === 'string' ? o.kind : '';
171
+ if (kind && !Object.keys(route.table).includes(kind)) {
172
+ fail(`--kind 须为 ${Object.keys(route.table).join('|')}(实际「${kind}」,值域读自 ${route.source})`);
173
+ }
174
+
175
+ const files = typeof o.files === 'string'
176
+ ? o.files.split(/[,,]/).map((s) => s.trim()).filter(Boolean)
177
+ : [];
178
+ const proposed = typeof o.level === 'string' ? o.level.toUpperCase() : '';
179
+ if (proposed && !levelList.includes(proposed)) {
180
+ fail(`--level 须为 ${levelList.join('|')}(实际「${proposed}」)`);
181
+ }
182
+
183
+ const a = crit.assessLevel(target, { files, proposed });
184
+ const level = a.level || 'L1';
185
+ // 泳道取法沿装户 stage-gates 单源(laneOfDoc:级别单字段协议),不在本文件复刻 low/high 口径。
186
+ // 装户无 stage-gates.mjs(老版本未升级)→ fail-soft 回退默认(判 L2/L3 → high),并登记降级。
187
+ const laneKey = await laneOfDoc(target, level);
188
+ if (laneKey.source === 'fallback') {
189
+ degradations.push({ code: 'lane-source-fallback', detail: `${laneKey.reason || '装户 .agents/scripts/stage-gates.mjs 缺失或 laneOfDoc 不可用'}——泳道按档位回落判定(建议 flow-kit sync 升级)` });
190
+ }
191
+ if (a.hintsConfigured === false) {
192
+ degradations.push({ code: 'level-hints-empty', detail: `.agents/level-hints.txt 未配置(缺失或为空)→ 机器面给不出档位提示,本次按 ${level} 起步;该清单登记「哪些路径一碰就至少是 L2/L3」(项目私有,只收 L2/L3 两档)` });
193
+ }
194
+ if (a.underHint) {
195
+ degradations.push({ code: 'under-hint-proposed', detail: `建议档 ${a.proposed} 低于路径提示下限 ${a.hintLevel}(命中:${(a.matched || []).slice(0, 4).join('、')})——已按就高不就低取 ${a.hintLevel},建议复核` });
196
+ }
197
+ const lane = laneKey.key === 'high' ? '防御道(同步确认门)' : '协作道(快车道)';
198
+ const entry = route.table[kind] || route.table.require;
199
+
200
+ if (o.json) {
201
+ console.log(JSON.stringify({
202
+ level, lane, laneKey: laneKey.key, laneSource: laneKey.source, kind: kind || '(未定)',
203
+ files, proposed: a.proposed, hintLevel: a.hintLevel, matched: a.matched,
204
+ underHint: a.underHint, hintsConfigured: a.hintsConfigured,
205
+ // 路由表来源(B5):'new-task.md' = 真取到装户机器单源;'fallback' = 包内兜底表(已登记降级)
206
+ routeTableSource: route.source,
207
+ // 降级统一出口(消费方判「这次路由是否在降级态」只需看这一个字段,不必逐个猜 source 字段)
208
+ degraded: degradations.length > 0, degradations,
209
+ nextStage: entry, nextCommand: `.agents/commands/${entry}`,
210
+ modules,
211
+ }, null, 2));
212
+ return;
213
+ }
214
+
215
+ console.log(`flow-kit route — 手动路由判定(机器面)\n`);
216
+ console.log(crit.routeAdvice(level, a));
217
+ console.log('');
218
+ console.log(`泳道:${lane}`);
219
+ if (kind) console.log(`入口指令:.agents/commands/${entry}(kind=${kind})`);
220
+ else console.log(`入口指令:未定 —— 先判 kind(require 新需求 / fix 修复 / verify 验证 / review 评审 / deploy 上线)`);
221
+ console.log(` new-task 阶段索引:.agents/commands/new-task.md`);
222
+ if (degradations.length) {
223
+ console.log('');
224
+ console.log(`⚠️ 降级运行(${degradations.length} 项——机器面能给的部分比完整态少,逐条点名):`);
225
+ for (const d of degradations) console.log(` · [${d.code}] ${d.detail}`);
226
+ }
227
+ console.log('');
228
+ console.log('本命令只给机器可判的部分。**定级仍需语义判断**——按上面自查句逐条判;');
229
+ console.log('不确定就高不就低。要走全自动六阶段闭环另用:flow-kit run start "<需求>"');
230
+ }
231
+
232
+ // 薄壳自有参数:这些是包 CLI 的选项,**不**透传给装户状态机(状态机有自己的参数协议)
233
+ const SHELL_ONLY_KEYS = new Set(['dir']);
234
+
235
+ export function runPipeline(args) {
236
+ const o = parseArgs(args);
237
+ const sub = o._[0];
238
+ // 用法校验先于脚本存在性校验:先告诉用户「怎么用」,再谈装没装(反过来会拿安装问题盖住用法)
239
+ if (!sub) {
240
+ fail('用法:flow-kit run <start|next|status|watch|abort> [...]\n' +
241
+ ' 驱动协议与停机点语义见 .agents/commands/pipeline-run.md(该文件是宿主面权威,本命令只透传)。');
242
+ }
243
+ const target = resolveTarget(o);
244
+ const script = requireInstalledScript(target, `${AGENT_SCRIPTS_REL}/pipeline-run.mjs`);
245
+ // 透传 = 用户给的参数原样交给装户状态机,但**剔除薄壳自有参数**(--dir 是包面寻址,不是状态机参数)
246
+ const passthrough = [];
247
+ for (let i = args.indexOf(sub) + 1; i < args.length; i++) {
248
+ const a = args[i];
249
+ if (a.startsWith('--')) {
250
+ const k = a.slice(2).split('=')[0];
251
+ if (SHELL_ONLY_KEYS.has(k)) {
252
+ if (!a.includes('=') && args[i + 1] !== undefined && !args[i + 1].startsWith('--')) i++;
253
+ continue;
254
+ }
255
+ }
256
+ passthrough.push(a);
257
+ }
258
+ const r = spawnSync(process.execPath, [script, sub, ...passthrough], {
259
+ cwd: target, stdio: 'inherit', windowsHide: true,
260
+ });
261
+ process.exit(r.status ?? 1);
262
+ }
@@ -0,0 +1,295 @@
1
+ // executor.test.mjs — flow-kit route / run 薄壳套件(2026-10-13 flow-kit-route)
2
+ // 方法:子进程 driver 调真实 src/executor.mjs(route 内部会 process.exit,隔离到子进程才能测退出码与 stdout);
3
+ // 装户 fixture 只放薄壳**依赖面**文件(level-criteria.mjs 真拷、stage-gates/pipeline-run 用哨兵 stub)。
4
+ // 覆盖:route 档位与入口映射 / hints 抬档与 underHint / 泳道真取装户单源(缺失时回落并标注)/
5
+ // --kind·--level 非法值 fail-loud / 缺判据单源 fail-loud / --json 机读面 /
6
+ // run 参数·退出码透传 / run 缺子命令用法提示 / run 缺 pipeline-run 指向 init-sync。
7
+ // 用法:node src/executor.test.mjs
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
+
14
+ const SRC = path.dirname(fileURLToPath(import.meta.url));
15
+ const PKG = path.resolve(SRC, '..');
16
+ const REAL_CRIT = path.join(PKG, 'templates/_agents/scripts/level-criteria.mjs');
17
+ const REAL_ENUMS_MJS = path.join(PKG, 'templates/_agents/scripts/workflow-enums.mjs');
18
+ const REAL_ENUMS_TXT = path.join(PKG, 'templates/_agents/workflow-enums.txt');
19
+
20
+ let pass = 0, failCount = 0;
21
+ function check(name, cond, detail = '') {
22
+ if (cond) { pass++; console.log(` ok ${name}`); }
23
+ else { failCount++; console.error(`FAIL ${name}${detail ? ' — ' + detail : ''}`); }
24
+ }
25
+ const W = (p, content) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, content, 'utf8'); };
26
+
27
+ // 装户 fixture:判据单源与枚举单源真拷;stage-gates / pipeline-run 用哨兵 stub(证明薄壳是「调用装户」而非自带实现)
28
+ // withEnums 默认 true = 完整装户面。降级通道(2026-10-13)要求能断言「完整态无降级」,
29
+ // 若夹具恒缺枚举单源,enums-fallback 会恒真、该断言即假护栏。
30
+ function mkProject({ withCrit = true, withStageGates = true, withEnums = true, withRouteTable = true, laneKey = null, hints = null, modules = null } = {}) {
31
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-exec-'));
32
+ const S = path.join(root, '.agents', 'scripts');
33
+ if (withCrit) fs.mkdirSync(S, { recursive: true }), fs.copyFileSync(REAL_CRIT, path.join(S, 'level-criteria.mjs'));
34
+ if (withStageGates) {
35
+ // 默认哨兵:按 L2/L3 → high 的常规口径(写在被写入的 stub 文件里,在装户侧求值)
36
+ const body = laneKey
37
+ ? `return ${JSON.stringify(laneKey)};`
38
+ : `return (level === 'L2' || level === 'L3') ? 'high' : 'low';`;
39
+ W(path.join(S, 'stage-gates.mjs'), `export function laneOfDoc(level){ ${body} }\n`);
40
+ }
41
+ if (hints !== null) W(path.join(root, '.agents', 'level-hints.txt'), hints);
42
+ if (modules !== null) W(path.join(root, '.agents', 'workflow-modules.txt'), modules);
43
+ if (withEnums) {
44
+ fs.mkdirSync(S, { recursive: true });
45
+ fs.copyFileSync(REAL_ENUMS_MJS, path.join(S, 'workflow-enums.mjs'));
46
+ fs.copyFileSync(REAL_ENUMS_TXT, path.join(root, '.agents', 'workflow-enums.txt'));
47
+ }
48
+ // 完整装户必带 route-kinds 机器单源(B5):kind→入口映射的权威在 new-task.md,包内不再自带一份
49
+ if (withRouteTable) {
50
+ fs.mkdirSync(path.join(root, '.agents', 'commands'), { recursive: true });
51
+ fs.writeFileSync(path.join(root, '.agents/commands/new-task.md'),
52
+ '## 3. 路由判断\n\n<!-- route-kinds: require=plan.md | fix=maintain.md | verify=test.md | review=review.md | deploy=deploy.md -->\n', 'utf8');
53
+ }
54
+ return root;
55
+ }
56
+
57
+ function runCli(args, cwd) {
58
+ // win32 绝对路径必须转 file:// URL,否则 ESM loader 判 ERR_UNSUPPORTED_ESM_URL_SCHEME(protocol 'e:')
59
+ const href = pathToFileURL(path.join(SRC, 'executor.mjs')).href;
60
+ const driver = `import(${JSON.stringify(href)})`
61
+ + `.then(m=>m.${args[0] === 'run' ? 'runPipeline' : 'route'}(${JSON.stringify(args.slice(1))}))`;
62
+ return spawnSync(process.execPath, ['-e', driver], { cwd, encoding: 'utf8', windowsHide: true });
63
+ }
64
+ const route = (args, cwd) => runCli(['route', ...args], cwd);
65
+ const run = (args, cwd) => runCli(['run', ...args], cwd);
66
+
67
+ // ============ 场景 1:无 hints(默认装户)→ L1 起步 + 明示未配置 ============
68
+ {
69
+ const r = mkProject();
70
+ const x = route(['--kind', 'require'], r);
71
+ check('S1 exit=0', x.status === 0, `status=${x.status} ${x.stderr}`);
72
+ check('S1 给出 L1', x.stdout.includes('建议级别:L1'));
73
+ check('S1 明示机器面给不出档位(不假装判得出)', x.stdout.includes('未配置') && x.stdout.includes('L1 起步'));
74
+ check('S1 带 level-hints.txt 路径指引', x.stdout.includes('.agents/level-hints.txt'));
75
+ check('S1 输出 L1 负例面', x.stdout.includes('单模块内部重构'));
76
+ check('S1 输出自查句', x.stdout.includes('能否列出'));
77
+ check('S1 kind=require → plan.md 入口', x.stdout.includes('.agents/commands/plan.md'));
78
+ check('S1 泳道=协作道', x.stdout.includes('泳道:协作道'));
79
+ }
80
+
81
+ // ============ 场景 2:kind → 入口 md 映射(五个值全覆盖) ============
82
+ {
83
+ const r = mkProject();
84
+ const map = { require: 'plan.md', fix: 'maintain.md', verify: 'test.md', review: 'review.md', deploy: 'deploy.md' };
85
+ for (const [kind, md] of Object.entries(map)) {
86
+ const x = route(['--kind', kind, '--json'], r);
87
+ const j = JSON.parse(x.stdout);
88
+ check(`S2 kind=${kind} → ${md}`, j.nextCommand === `.agents/commands/${md}`, JSON.stringify(j.nextCommand));
89
+ }
90
+ const none = JSON.parse(route(['--json'], r).stdout);
91
+ check('S2 未给 kind → 默认 require 且显式标记未定', none.kind === '(未定)' && none.nextStage === 'plan.md');
92
+ check('S2 未给 kind 的文本面提示先判 kind', route([], r).stdout.includes('先判 kind'));
93
+ }
94
+
95
+ // ============ 场景 3:hints 抬档 + underHint ============
96
+ {
97
+ const r = mkProject({ hints: 'L3 ^src/db/migrations/.*\\.sql$\nL2 ^src/contracts/\n' });
98
+ const hit = route(['--files', 'src/db/migrations/001_init.sql', '--json'], r);
99
+ const hj = JSON.parse(hit.stdout);
100
+ check('S3 hints 命中抬到 L3', hj.level === 'L3' && hj.hintLevel === 'L3');
101
+ check('S3 matched 带命中证据', hj.matched.length > 0 && hj.matched[0].includes('001_init.sql'), JSON.stringify(hj.matched));
102
+ check('S3 L2 hints 抬到 L2', JSON.parse(route(['--files', 'src/contracts/t.ts', '--json'], r).stdout).level === 'L2');
103
+ const low = JSON.parse(route(['--level', 'L1', '--files', 'src/db/migrations/002_x.sql', '--json'], r).stdout);
104
+ check('S3 建议档低于下限 → underHint=true', low.underHint === true);
105
+ check('S3 就高不就低:level 仍取下限 L3', low.level === 'L3');
106
+ check('S3 文本面输出改判警告', route(['--level', 'L1', '--files', 'src/db/migrations/002_x.sql'], r).stdout.includes('低于路径提示下限'));
107
+ const miss = JSON.parse(route(['--files', 'src/ui/b.tsx', '--json'], r).stdout);
108
+ check('S3 未命中 → L1 且 hintLevel 空', miss.level === 'L1' && miss.hintLevel === '' && miss.matched.length === 0);
109
+ check('S3 多文件逗号/中文逗号切分', JSON.parse(route(['--files', 'src/a.ts,src/contracts/t.ts', '--json'], r).stdout).level === 'L2');
110
+ // 寻址:--dir=<path> 与 --dir <path> 等价(后者被 parseArgs 消费后不能落进 files)
111
+ const eqDir = route([`--dir=${r}`, '--kind', 'require', '--files', 'src/contracts/t.ts', '--json'], os.tmpdir());
112
+ check('S3 --dir= 形式定位装户根', JSON.parse(eqDir.stdout).level === 'L2', eqDir.stderr);
113
+ check('S3 --dir 不被误当改动文件', JSON.parse(eqDir.stdout).files.length === 1, eqDir.stdout);
114
+ }
115
+
116
+ // ============ 场景 4:泳道真取装户单源 ============
117
+ {
118
+ // 哨兵:装户 laneOfDoc 对 L1 返回 low、对 L2 返回 high —— 若薄壳自带实现(按档写死),
119
+ // 换成「L2 返回 low」的哨兵即可测出:薄壳必须问装户,不能自己判。
120
+ const r = mkProject({ laneKey: null });
121
+ W(path.join(r, '.agents', 'scripts', 'stage-gates.mjs'),
122
+ 'export function laneOfDoc(level){ return level === "L2" ? "low" : "high"; }\n');
123
+ const j = JSON.parse(route(['--kind', 'require', '--level', 'L2', '--json'], r).stdout);
124
+ check('S4 泳道取装户单源返回值(不按档写死)', j.laneKey === 'low', `laneKey=${j.laneKey}`);
125
+ check('S4 laneSource 标明单源', j.laneSource === 'stage-gates.mjs', j.laneSource);
126
+ check('S4 low → 协作道', j.lane === '协作道(快车道)');
127
+ const j2 = JSON.parse(route(['--level', 'L3', '--json'], r).stdout);
128
+ check('S4 high → 防御道', j2.laneKey === 'high' && j2.lane.includes('防御道'));
129
+
130
+ // 单源返回非法值 → 不假装取到,回落并标注
131
+ const bad = mkProject();
132
+ W(path.join(bad, '.agents', 'scripts', 'stage-gates.mjs'), 'export function laneOfDoc(){ return "banana"; }\n');
133
+ const bj = JSON.parse(route(['--level', 'L2', '--json'], bad).stdout);
134
+ check('S4 单源返回非法值 → 回落', bj.laneKey === 'high' && bj.laneSource === 'fallback', JSON.stringify(bj.laneKey));
135
+
136
+ // 单源缺失 → 回落 + 降级通道可见(2026-10-13 degradation-channel:文本与 JSON 同一出口)
137
+ const miss = mkProject({ withStageGates: false });
138
+ const mx = route(['--level', 'L2'], miss);
139
+ check('S4 单源缺失 → 回落标注', mx.stdout.includes('lane-source-fallback'), mx.stdout.slice(-200));
140
+ check('S4 降级文本面统一出口', mx.stdout.includes('降级运行'));
141
+ check('S4 缺单源不阻断路由(仍给档位与入口)', mx.stdout.includes('建议级别:L2') && mx.status === 0);
142
+ const mj = JSON.parse(route(['--level', 'L2', '--json'], miss).stdout);
143
+ check('S4 缺单源时 JSON 面标 fallback', mj.laneSource === 'fallback');
144
+ check('S4 JSON 降级数组含 lane-source-fallback', mj.degraded === true && mj.degradations.some((d) => d.code === 'lane-source-fallback'), JSON.stringify(mj.degradations));
145
+ }
146
+
147
+ // ============ 场景 4b:降级通道完整性(此前四种缺失策略混用、只有 laneSource 可见)============
148
+ {
149
+ const codesOf = (proj, args) => JSON.parse(route(args.concat(['--json']), proj).stdout).degradations.map((d) => d.code);
150
+
151
+ // 判据单源齐全、无 hints → level-hints-empty 必须登记(此前对 JSON 消费方完全静默)
152
+ const noHints = mkProject();
153
+ const c1 = codesOf(noHints, ['--level', 'L1']);
154
+ check('S4b 未配 hints → 降级登记 level-hints-empty', c1.includes('level-hints-empty'), JSON.stringify(c1));
155
+ check('S4b 配了 hints 后不再报该降级', (() => {
156
+ fs.writeFileSync(path.join(noHints, '.agents', 'level-hints.txt'), '# x\nL2 ^src/db/\n');
157
+ return !codesOf(noHints, ['--level', 'L1']).includes('level-hints-empty');
158
+ })());
159
+
160
+ // 枚举单源缺失 → enums-fallback(此前静默用自带表)
161
+ const noEnums = mkProject({ withEnums: false });
162
+ check('S4b 枚举缺失 → 降级登记 enums-fallback', codesOf(noEnums, ['--level', 'L1']).includes('enums-fallback'), JSON.stringify(codesOf(noEnums, ['--level', 'L1'])));
163
+
164
+ // 建议档低于 hints 下限 → under-hint-proposed(此前仅 warning 字段,无统一出口)
165
+ const under = mkProject();
166
+ fs.writeFileSync(path.join(under, '.agents', 'level-hints.txt'), 'L3 ^src/db/\n');
167
+ const c3 = codesOf(under, ['--level', 'L1', '--files', 'src/db/schema.sql']);
168
+ check('S4b 判低 → 降级登记 under-hint-proposed 且仍按就高不就低取 L3', c3.includes('under-hint-proposed')
169
+ && JSON.parse(route(['--level', 'L1', '--files', 'src/db/schema.sql', '--json'], under).stdout).level === 'L3', JSON.stringify(c3));
170
+
171
+ // 完整态(hints + 枚举 + 单源齐全)→ degraded=false,降级数组为空
172
+ const full = mkProject();
173
+ fs.writeFileSync(path.join(full, '.agents', 'level-hints.txt'), 'L2 ^src/db/\n');
174
+ const fj = JSON.parse(route(['--level', 'L1', '--json'], full).stdout);
175
+ check('S4b 完整态无降级', fj.degraded === false && fj.degradations.length === 0, JSON.stringify(fj.degradations));
176
+ }
177
+
178
+ // ============ 场景 5:非法输入 fail-loud ============
179
+ {
180
+ const r = mkProject();
181
+ const k = route(['--kind', 'bogus'], r);
182
+ check('S5 非法 kind exit=1', k.status === 1);
183
+ check('S5 非法 kind 报文列合法值', k.stderr.includes('require|fix|verify|review|deploy'), k.stderr);
184
+ const l = route(['--level', 'L9'], r);
185
+ check('S5 非法 level exit=1 且列闭集', l.status === 1 && l.stderr.includes('L1|L2|L3'), l.stderr);
186
+ // L0 已退役(2026-10-13 自源仓收敛系列 1 回灌):--level L0 按非法档 fail-loud(枚举 fixture 已收敛 L1-L3)
187
+ const l0 = route(['--level', 'L0'], r);
188
+ check('S5b 已退役的 L0 exit=1 且报文即闭集', l0.status === 1 && l0.stderr.includes('--level 须为 L1|L2|L3'), l0.stderr);
189
+ const nc = mkProject({ withCrit: false });
190
+ const n = route([], nc);
191
+ check('S5 缺判据单源 exit=1', n.status === 1);
192
+ check('S5 缺判据单源指向 sync 升级(不自动改盘面)', n.stderr.includes('flow-kit sync'), n.stderr);
193
+ const badHints = route(['--files', 'src/a.ts'], mkProject({ hints: 'L9 ^src/\n' }));
194
+ check('S5 非法 hints 配置 → exit=1 且报错定位', badHints.status === 1 && badHints.stderr.includes('level-hints.txt'));
195
+ }
196
+
197
+ // ============ 场景 6:--json 机读面 ============
198
+ {
199
+ const r = mkProject({ hints: 'L2 ^src/contracts/\n', modules: '# 注释\nbilling\norder\n' });
200
+ const x = route(['--kind', 'fix', '--files', 'src/contracts/t.ts', '--json'], r);
201
+ check('S6 exit=0 且 stdout 是纯 JSON', x.status === 0 && (() => { try { JSON.parse(x.stdout); return true; } catch { return false; } })(), x.stdout.slice(0, 120));
202
+ const j = JSON.parse(x.stdout);
203
+ check('S6 字段齐备', ['level', 'lane', 'laneKey', 'laneSource', 'kind', 'files', 'proposed', 'hintLevel', 'matched', 'underHint', 'hintsConfigured', 'nextStage', 'nextCommand', 'modules']
204
+ .every((k) => k in j), Object.keys(j).join(','));
205
+ check('S6 modules 读装户词表(去注释)', Array.isArray(j.modules) && j.modules.length === 2 && !j.modules.includes('# 注释'), JSON.stringify(j.modules));
206
+ check('S6 JSON 面不含人读散文(判据面归单源)', !x.stdout.includes('能否列出'));
207
+ }
208
+
209
+ // ============ 场景 7:run 透传(参数 + 退出码 + cwd) ============
210
+ {
211
+ const r = mkProject();
212
+ W(path.join(r, '.agents', 'scripts', 'pipeline-run.mjs'),
213
+ 'const a = process.argv.slice(2);\n'
214
+ + 'console.log("STUB_PIPE " + JSON.stringify(a) + " cwd=" + process.cwd());\n'
215
+ + 'process.exit(a[0] === "boom" ? 7 : 0);\n');
216
+ const st = run(['status'], r);
217
+ check('S7 status 透传成功 exit=0', st.status === 0, st.stderr);
218
+ check('S7 参数原样送达装户状态机', st.stdout.includes('STUB_PIPE ["status"]'), st.stdout.trim());
219
+ check('S7 cwd = 装户根', st.stdout.includes(path.resolve(r).replace(/\\/g, '\\')), st.stdout.trim());
220
+ const nx = run(['next'], r);
221
+ check('S7 next 透传', nx.stdout.includes('["next"]'));
222
+ const bo = run(['boom'], r);
223
+ check('S7 装户非零退出码原样回传', bo.status === 7, `status=${bo.status}`);
224
+ const args = run(['start', '给导入功能加审计字段'], r);
225
+ check('S7 含空格的需求原话不被拆词', args.stdout.includes('["start","给导入功能加审计字段"]'), args.stdout.trim());
226
+ const withDir = run(['status', '--dir', r], os.tmpdir());
227
+ check('S7 --dir 被薄壳消费、不透传状态机', withDir.stdout.includes('["status"]'), withDir.stdout.trim());
228
+ check('S7 --dir 仍定位到装户根', withDir.stdout.includes('STUB_PIPE') && withDir.status === 0, withDir.stderr);
229
+ const dirEq = run([`--dir=${r}`, 'status'], os.tmpdir());
230
+ check('S7 --dir= 形式同样被消费', dirEq.stdout.includes('["status"]'), dirEq.stdout.trim());
231
+ }
232
+
233
+ // ============ 场景 8:run 的 fail-loud 与边界 ============
234
+ {
235
+ const r = mkProject();
236
+ const noSub = run([], r);
237
+ check('S8 缺子命令 exit=1', noSub.status === 1);
238
+ check('S8 缺子命令列子命令闭集', noSub.stderr.includes('start|next|status|watch|abort'), noSub.stderr);
239
+ check('S8 缺子命令指向宿主面驱动协议权威', noSub.stderr.includes('.agents/commands/pipeline-run.md'), noSub.stderr);
240
+ const bare = mkProject();
241
+ fs.unlinkSync(path.join(bare, '.agents', 'scripts', 'level-criteria.mjs'));
242
+ const miss = run(['status'], bare);
243
+ check('S8 缺 pipeline-run exit=1', miss.status === 1);
244
+ check('S8 缺脚本指向 init/sync(不静默装一遍)', miss.stderr.includes('flow-kit init') && miss.stderr.includes('flow-kit sync'), miss.stderr);
245
+ const notInstalled = run(['status'], mkProject({ withCrit: false }));
246
+ check('S8 非装户目录 → 明确报未安装', notInstalled.stderr.includes('不是已安装 flow-kit 的项目'), notInstalled.stderr);
247
+ }
248
+
249
+ // ============ 场景 9:薄壳纪律 —— 包面不复制判据/不自带实现 ============
250
+ {
251
+ const ex = fs.readFileSync(path.join(SRC, 'executor.mjs'), 'utf8');
252
+ check('S9 包面不硬编码判据散文', !ex.includes('聚合投影键') && !ex.includes('能否列出'));
253
+ check('S9 包面不硬编码泳道 low/high 口径', !/level === 'L2' \|\| level === 'L3'/.test(ex));
254
+ // 纪律只查**代码里的引用形式**(引号包裹的实参),不查注释散文——注释里出现「不带 --no-verify」是
255
+ // 纪律声明本身,不是违规;把它也算违规等于逼人删掉自述。
256
+ check('S9 薄壳代码不出现 --no-verify 实参', !ex.includes("'--no-verify'") && !ex.includes('"--no-verify"'));
257
+ check('S9 薄壳不 spawn AI 会话', !/claude|codex|opencode|gemini/.test(ex));
258
+ check('S9 判据单源从装户面动态载入(不在包内拷一份)', ex.includes('level-criteria.mjs') && ex.includes('import('));
259
+ }
260
+
261
+ // ============ 场景 4c:kind→入口读装户权威表(B5:包内不再自带一份映射)============
262
+ {
263
+ // ① 默认夹具带 route-kinds 行 → routeTableSource 应为 new-task.md(非兜底)
264
+ const r = mkProject();
265
+ fs.mkdirSync(path.join(r, '.agents', 'commands'), { recursive: true });
266
+ fs.writeFileSync(path.join(r, '.agents', 'commands', 'new-task.md'),
267
+ '## 3. 路由判断\n\n<!-- route-kinds: require=plan.md | fix=maintain.md | verify=test.md | review=review.md | deploy=deploy.md -->\n', 'utf8');
268
+ const j = JSON.parse(route(['--kind', 'fix', '--json'], r).stdout);
269
+ check('S4c 路由表取自装户 new-task.md(非兜底)', j.routeTableSource === 'new-task.md', j.routeTableSource);
270
+ check('S4c kind=fix → 入口取自权威表', j.nextStage === 'maintain.md', j.nextStage);
271
+
272
+ // ② 装户把入口改了个名(模拟项目自定义)→ 包必须跟随,不得仍指向旧名
273
+ fs.writeFileSync(path.join(r, '.agents', 'commands', 'new-task.md'),
274
+ '<!-- route-kinds: require=plan.md | fix=hotfix.md | verify=test.md | review=review.md | deploy=deploy.md -->\n', 'utf8');
275
+ const j2 = JSON.parse(route(['--kind', 'fix', '--json'], r).stdout);
276
+ check('S4c 装户改入口名 → 包跟随权威表(无第二份硬编码)', j2.nextStage === 'hotfix.md', j2.nextStage);
277
+
278
+ // ③ 权威表读不到 → 回退包内默认表 + 登记降级(老版本装户无该行,不静默假装)
279
+ const r3 = mkProject({ withRouteTable: false });
280
+ const j3 = JSON.parse(route(['--kind', 'require', '--json'], r3).stdout);
281
+ check('S4c 无 route-kinds 行 → 回落 + 登记 route-table-fallback',
282
+ j3.routeTableSource === 'fallback' && j3.nextStage === 'plan.md'
283
+ && j3.degradations.some((d) => d.code === 'route-table-fallback'), JSON.stringify(j3.degradations));
284
+
285
+ // ④ kind 闭集随权威表走:表里没有的 kind 判红,报文点明值域来源
286
+ const r4 = mkProject();
287
+ fs.writeFileSync(path.join(r4, '.agents', 'commands', 'new-task.md'),
288
+ '<!-- route-kinds: require=plan.md | ship=deploy.md -->\n', 'utf8');
289
+ const bad = route(['--kind', 'fix'], r4);
290
+ check('S4c 权威表无该 kind → exit 1 且值域读自权威表',
291
+ bad.status === 1 && bad.stderr.includes('require|ship') && bad.stderr.includes('new-task.md'), bad.stderr);
292
+ }
293
+
294
+ console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
295
+ process.exit(failCount ? 1 : 0);
@@ -24,7 +24,7 @@ const r = spawnSync(process.execPath, [
24
24
  const out = `${r.stdout || ''}${r.stderr || ''}`;
25
25
  check('fresh init exit 0', r.status === 0, `exit=${r.status}\n${out.split('\n').slice(-30).join('\n')}`);
26
26
  const kit = JSON.parse(fs.readFileSync(path.join(dir, '.agents', 'kit.json'), 'utf8'));
27
- check('新装 audit 为 false 且 policyVersion 为 2', kit.audit === false && kit.policyVersion === 2, JSON.stringify({ audit: kit.audit, policyVersion: kit.policyVersion }));
27
+ check('新装 audit 为 false 且 policyVersion 为 12(2026-10-13 B6b-2 回灌对齐源仓 v12:新装即带全锚——§21–26 六段 + §3x + 复核上下文;新装空台账零回溯)', kit.audit === false && kit.policyVersion === 12, JSON.stringify({ audit: kit.audit, policyVersion: kit.policyVersion }));
28
28
  // 模板下发感知初始锚(2026-10-06 template-downstream):有包源模板对应的 owned 条目带 srcSha256=
29
29
  // 包源模板原文 LF 归一 sha;生成器目标(INDEX.md 类)排除出感知面无该键
30
30
  {
package/src/init.mjs CHANGED
@@ -243,9 +243,11 @@ export async function init(args, pkgRoot) {
243
243
  }
244
244
 
245
245
  // 4) .gitignore 追加(缺哪条补哪条)。2026-10-05 起七个宿主一律 localOnly:false,
246
- // 宿主目录不再自动进 .gitignore——实际只补 .agents/cache/ 这条运行时缓存。
246
+ // 宿主目录不再自动进 .gitignore——补 .agents/cache/ 运行时缓存与 .agents/judgments.jsonl
247
+ // 判定流水(2026-10-13 judgment-ledger 回灌 B4:check-loop 每次真实运行追加的本机运行判定,
248
+ // 各机各自产生不跨机共享,月度聚合经 agg-judgments.mjs)。
247
249
  const giPath = path.join(target, '.gitignore');
248
- const giNeed = ['.agents/cache/', ...(hosts.filter((h) => HOSTS[h].localOnly).map((h) => `${HOSTS[h].dir}/`))];
250
+ const giNeed = ['.agents/cache/', '.agents/judgments.jsonl', ...(hosts.filter((h) => HOSTS[h].localOnly).map((h) => `${HOSTS[h].dir}/`))];
249
251
  let giText = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
250
252
  const giAdd = giNeed.filter((l) => !giText.split(/\r?\n/).some((x) => x.trim() === l));
251
253
  if (giAdd.length) {
@@ -263,6 +265,13 @@ export async function init(args, pkgRoot) {
263
265
  execSync('git config merge.ours.driver true', { cwd: target, stdio: 'pipe' });
264
266
  console.log(' 已启用 merge.ours driver(.gitattributes 的 merge=ours 生效)');
265
267
  } catch { /* 可选配置,失败不阻断 */ }
268
+ try {
269
+ // 台账并集合并 driver(2026-10-12 ledger-merge-driver):确认台账是尾部追加形态,多人/多 worktree
270
+ // 并行时默认合并会给文本冲突,人工「整取一侧」会静默丢确认行。配了 driver 后合并 = 并集去重。
271
+ // 本机态配置不跨机共享(与 core.hooksPath 同款性质)——漏配则退化为文本冲突,不静默丢数据。
272
+ execSync('git config merge.jsonl-union.driver "node .agents/scripts/git-merge-jsonl.mjs %O %A %B"', { cwd: target, stdio: 'pipe' });
273
+ console.log(' 已启用 merge.jsonl-union driver(台账 .agents/*.jsonl 合并走并集去重,不再文本冲突)');
274
+ } catch { /* 可选配置,失败不阻断 */ }
266
275
  } catch {
267
276
  console.log(' ⚠️ 目标不是 git 仓库——钩子未挂载;git init 后手动执行:git config core.hooksPath .githooks');
268
277
  }
@@ -324,7 +333,11 @@ export async function init(args, pkgRoot) {
324
333
  fs.writeFileSync(kitPath, `${JSON.stringify({
325
334
  kit: 'agentic-flow-kit',
326
335
  version: pkg.version,
327
- policyVersion: 2,
336
+ // policyVersion 12(2026-10-13 B6b-2 回灌对齐源仓 v12):新装即带全锚——v9–v12 = incidentRegrSince /
337
+ // specVerifSince / attributionSince / convergenceSince / convergenceHardSince / reviewerSince
338
+ // (§21–26 六段 + §3x 防复发证据门 + 复核上下文子检)。新装空台账零回溯:各段锚前存量豁免、空仓零
339
+ // 噪声;v1–v11 存量装户不受影响(缺键 → 对应消费段整体跳过)。
340
+ policyVersion: 12,
328
341
  // 新装默认只跑硬规则。缺省(旧台账无此字段)在 check-loop 里仍是全量,避免升级后卫生检查突然消失。
329
342
  audit: false,
330
343
  // 门禁信任档(2026-10-12 gate-trust-level):默认 'project' —— gate 安装锚失配只 WARN,
@@ -340,6 +353,17 @@ export async function init(args, pkgRoot) {
340
353
  }, null, 2)}\n`);
341
354
  console.log(` kit.json 台账:managed ${managed.length} 份 | owned ${ownedFinal.length} 份(版本 ${pkg.version})`);
342
355
 
356
+ // 生效规则视图种子(2026-10-13 verify-gates 回灌):verify.mjs `effective-policy` 门 --check 对缺文件
357
+ // 判漂移(红)——源仓等价物是该生成物随生成器同笔入库;kit 新装由 init 首跑种子,保证 fresh 装户
358
+ // 首个 verify 不被卡。**必须在 kit.json 落盘之后**:渲染读 loadKitPolicy(.agents/kit.json 的
359
+ // policyVersion),先跑会按回落 v1 种子、装完即与 v12 视图漂移(fresh 实测)。本文件不入 owned 台账
360
+ // (生成器重写面,非模板渲染件),不受 P1-2「先跑后记账」约束;视图日期 = 安装日,--check 按磁盘
361
+ // 视图日期重渲染,跨锚日字节稳定。
362
+ if (guardRun('.agents/scripts/gen-effective-policy.mjs')) {
363
+ const genPolicy = runNode(target, '.agents/scripts/gen-effective-policy.mjs');
364
+ console.log(genPolicy.ok ? ' 已生成 workflow/effective-policy.md(生效规则视图——policy 变更后重生成并随同提交)' : ` ⚠️ gen-effective-policy 失败:${genPolicy.out.split('\n')[0]}`);
365
+ }
366
+
343
367
  // 8) 自检(kit.json 已按生成后盘面记账——doctor §6.6 应全绿,fresh init 以 exit 0 收场)
344
368
  // 输出语义(p2-batch1 P2-4):doctor 打印在异步端口回调里、晚于本横幅——脚本化消费方按退出码判定
345
369
  console.log('▶ flow-kit doctor(安装产物已就位;体检 FAIL 时本命令 exit 1,产物无损——按 FAIL 项处置)');