agentic-flow-kit 1.7.0 → 1.7.16

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 (162) hide show
  1. package/README.md +81 -9
  2. package/modules/gates/README.md +1 -0
  3. package/modules/gates/test-protect/README.md +23 -0
  4. package/modules/gates/test-protect/check-test-protect.mjs +64 -0
  5. package/modules/gates/test-protect/local-pre-commit.line +1 -0
  6. package/modules/hosts/README.md +57 -0
  7. package/modules/hosts/claude/commands/wf-build.md +5 -24
  8. package/modules/hosts/claude/commands/wf-deploy.md +1 -0
  9. package/modules/hosts/claude/commands/wf-design.md +7 -6
  10. package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
  11. package/modules/hosts/claude/commands/wf-new-task.md +42 -21
  12. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  13. package/modules/hosts/claude/commands/wf-review.md +1 -1
  14. package/modules/hosts/claude/commands/wf-sync-hosts.md +8 -16
  15. package/modules/hosts/claude/commands/wf-test.md +5 -4
  16. package/modules/hosts/codex/commands/wf-build.md +5 -24
  17. package/modules/hosts/codex/commands/wf-deploy.md +1 -0
  18. package/modules/hosts/codex/commands/wf-design.md +7 -6
  19. package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
  20. package/modules/hosts/codex/commands/wf-new-task.md +42 -21
  21. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  22. package/modules/hosts/codex/commands/wf-review.md +1 -1
  23. package/modules/hosts/codex/commands/wf-sync-hosts.md +8 -16
  24. package/modules/hosts/codex/commands/wf-test.md +5 -4
  25. package/modules/hosts/cursor/commands/wf-build.md +5 -24
  26. package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
  27. package/modules/hosts/cursor/commands/wf-design.md +7 -6
  28. package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
  29. package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
  30. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  31. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  32. package/modules/hosts/cursor/commands/wf-sync-hosts.md +8 -16
  33. package/modules/hosts/cursor/commands/wf-test.md +5 -4
  34. package/modules/hosts/opencode/commands/wf-build.md +5 -24
  35. package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
  36. package/modules/hosts/opencode/commands/wf-design.md +7 -6
  37. package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
  38. package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
  39. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  40. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  41. package/modules/hosts/opencode/commands/wf-sync-hosts.md +8 -16
  42. package/modules/hosts/opencode/commands/wf-test.md +5 -4
  43. package/modules/hosts/trae/commands/wf-build.md +5 -24
  44. package/modules/hosts/trae/commands/wf-deploy.md +1 -0
  45. package/modules/hosts/trae/commands/wf-design.md +7 -6
  46. package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
  47. package/modules/hosts/trae/commands/wf-new-task.md +42 -21
  48. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  49. package/modules/hosts/trae/commands/wf-review.md +1 -1
  50. package/modules/hosts/trae/commands/wf-sync-hosts.md +8 -16
  51. package/modules/hosts/trae/commands/wf-test.md +5 -4
  52. package/package.json +2 -2
  53. package/src/add-gate.mjs +25 -1
  54. package/src/add-gate.test.mjs +103 -0
  55. package/src/cli.mjs +59 -5
  56. package/src/doctor.mjs +136 -12
  57. package/src/executor.mjs +245 -0
  58. package/src/executor.test.mjs +292 -0
  59. package/src/gates.test.mjs +17 -0
  60. package/src/init.mjs +14 -1
  61. package/src/init.test.mjs +19 -0
  62. package/src/instance-ref.test.mjs +62 -0
  63. package/src/jscpd-drift.test.mjs +144 -0
  64. package/src/profiles.mjs +3 -0
  65. package/src/render.mjs +6 -1
  66. package/src/rule-budget.test.mjs +103 -0
  67. package/src/sync-hosts.mjs +41 -45
  68. package/src/sync-hosts.test.mjs +67 -56
  69. package/src/trae-hooks.test.mjs +15 -3
  70. package/src/validate.mjs +72 -0
  71. package/src/validate.test.mjs +95 -0
  72. package/templates/.gitattributes +8 -0
  73. package/templates/AGENTS.md +2 -1
  74. package/templates/_agents/commands/build.md +5 -24
  75. package/templates/_agents/commands/deploy.md +1 -0
  76. package/templates/_agents/commands/design.md +7 -6
  77. package/templates/_agents/commands/gate-checklist.md +1 -3
  78. package/templates/_agents/commands/new-task.md +42 -21
  79. package/templates/_agents/commands/plan.md +1 -1
  80. package/templates/_agents/commands/review.md +1 -1
  81. package/templates/_agents/commands/sync-hosts.md +8 -16
  82. package/templates/_agents/commands/test.md +5 -4
  83. package/templates/_agents/control-surfaces.txt +53 -0
  84. package/templates/_agents/engine-lock.json +246 -0
  85. package/templates/_agents/lane-surfaces.txt +1 -1
  86. package/templates/_agents/level-hints.txt +30 -0
  87. package/templates/_agents/scripts/agg-delegations.cjs +51 -6
  88. package/templates/_agents/scripts/agg-delegations.test.mjs +84 -4
  89. package/templates/_agents/scripts/check-engine-integrity.mjs +217 -0
  90. package/templates/_agents/scripts/check-engine-integrity.test.mjs +215 -0
  91. package/templates/_agents/scripts/check-lane-surface.mjs +78 -24
  92. package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
  93. package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
  94. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
  95. package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
  96. package/templates/_agents/scripts/check-loop.mjs +255 -45
  97. package/templates/_agents/scripts/check-loop.test.mjs +330 -15
  98. package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
  99. package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
  100. package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
  101. package/templates/_agents/scripts/confirm-doc.mjs +54 -12
  102. package/templates/_agents/scripts/confirm-doc.test.mjs +59 -0
  103. package/templates/_agents/scripts/doctor.test.mjs +294 -1
  104. package/templates/_agents/scripts/draft-sediment.mjs +178 -0
  105. package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
  106. package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
  107. package/templates/_agents/scripts/fill-intent.mjs +2 -2
  108. package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
  109. package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
  110. package/templates/_agents/scripts/gate-checklist.mjs +6 -3
  111. package/templates/_agents/scripts/gate-checklist.test.mjs +2 -5
  112. package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
  113. package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
  114. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +10 -13
  115. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -30
  116. package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
  117. package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
  118. package/templates/_agents/scripts/kb-search.mjs +87 -12
  119. package/templates/_agents/scripts/kb-search.test.mjs +83 -0
  120. package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
  121. package/templates/_agents/scripts/ledger-loader.mjs +108 -0
  122. package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
  123. package/templates/_agents/scripts/ledger-prune.mjs +96 -0
  124. package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
  125. package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
  126. package/templates/_agents/scripts/level-criteria.mjs +232 -0
  127. package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
  128. package/templates/_agents/scripts/pipeline-run.mjs +5 -3
  129. package/templates/_agents/scripts/policy.mjs +47 -0
  130. package/templates/_agents/scripts/solidify-task.mjs +1 -1
  131. package/templates/_agents/scripts/stage-gates.mjs +52 -10
  132. package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
  133. package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
  134. package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
  135. package/templates/_agents/scripts/verify.mjs +1 -1
  136. package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
  137. package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
  138. package/templates/_agents/scripts/workflows-check.mjs +1 -1
  139. package/templates/_agents/scripts-test-exempt.txt +3 -1
  140. package/templates/_agents/skills/wiki/SKILL.md +5 -3
  141. package/templates/_agents/workflows/pipeline-closing.md +4 -5
  142. package/templates/_githooks/pre-commit +3 -34
  143. package/templates/_githooks/pre-push +3 -3
  144. package/templates/wiki/INDEX.md +44 -1
  145. package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
  146. package/templates/workflow/README.md +11 -0
  147. package/templates/workflow/delegations.md +5 -4
  148. package/templates/workflow/incidents/_TEMPLATE.md +2 -0
  149. package/templates/workflow/intents/_TEMPLATE.md +3 -0
  150. package/templates/workflow/plans/_TEMPLATE.md +4 -0
  151. package/templates/workflow/specs/_TEMPLATE.md +23 -0
  152. package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -91
  153. package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -91
  154. package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -91
  155. package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -91
  156. package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -92
  157. package/templates/_agents/commands/source-sync-check.md +0 -105
  158. package/templates/_agents/scripts/check-ledger.mjs +0 -109
  159. package/templates/_agents/scripts/check-ledger.test.mjs +0 -197
  160. package/templates/_agents/scripts/source-sync-check.mjs +0 -156
  161. package/templates/_agents/scripts/source-sync-check.test.mjs +0 -281
  162. package/templates/_agents/workflows/source-sync-repair.md +0 -30
package/src/doctor.mjs CHANGED
@@ -6,6 +6,7 @@ import net from 'node:net';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { listTree, scriptTrusted } from './render.mjs';
8
8
  import { isOwned, HOSTS, pickStackVars, srcTemplatePath, templateDriftOf, TEMPLATE_DRIFT_EXCLUDE } from './profiles.mjs';
9
+ import { bodySha } from './sync-hosts.mjs';
9
10
 
10
11
  // 宿主目录映射(与 src/profiles.mjs#HOSTS 同源:装副本目录名前缀带点)
11
12
  const HOST_DIR = Object.fromEntries(Object.entries(HOSTS).map(([k, v]) => [k, v.dir]));
@@ -178,6 +179,20 @@ export function doctor(args, pkgRoot) {
178
179
  else add('WARN', `workflow/INDEX.md 漂移——跑 node .agents/scripts/gen-workflow-index.mjs 重生成`);
179
180
  }
180
181
 
182
+ // 6b. wiki 索引漂移(2026-10-10:镜像 §6 workflow 侧——wiki INDEX 计数/映射表/看板 DATA 原本无任何
183
+ // 机器校验,装户手改生成区或忘跑 gen-wiki-board 只有肉眼能发现;workflow 侧有 doctor + check-loop 11
184
+ // 双路,wiki 侧此前是单路都没有)。存在性先行:旧装户无此脚本 = 既有「静默跳过」语义(同 §6.5 口径)
185
+ if (fs.existsSync(path.join(target, '.agents/scripts/gen-wiki-board.mjs'))) {
186
+ const wikiGuard = scriptGuard('.agents/scripts/gen-wiki-board.mjs');
187
+ if (!wikiGuard.ok) {
188
+ add('WARN', `wiki/INDEX.md 漂移检查跳过——${wikiGuard.note}(供应链防线)`);
189
+ } else {
190
+ const wb = spawnSync(process.execPath, ['.agents/scripts/gen-wiki-board.mjs', '--check'], { cwd: target, encoding: 'utf8' });
191
+ if (wb.status === 0) add('PASS', 'wiki/INDEX.md 与看板无漂移');
192
+ else add('WARN', `wiki/INDEX.md 或看板漂移——跑 node .agents/scripts/gen-wiki-board.mjs 重生成`);
193
+ }
194
+ }
195
+
181
196
  // 6.5 delegations 台账结构(量化层非门禁:结构漂移曾静默吞掉全部记录,2026-09-24)
182
197
  // 存在性先行(复核 P2-2):旧装户无此脚本 = 既有「静默跳过」语义,不产生防线 WARN 噪音
183
198
  if (fs.existsSync(path.join(target, '.agents/scripts/agg-delegations.cjs'))) {
@@ -214,11 +229,35 @@ export function doctor(args, pkgRoot) {
214
229
  add('PASS', `owned ${ownedRes.total} 份无漂移(装副本与台账 sha 对齐)`);
215
230
  } else {
216
231
  // owned 漂移已严化为 FAIL(2026-09-25 doctor-owned-drift-strict 复盘;首次引入用 WARN,installed 装户吃过警告后升级)
217
- if (ownedRes.drift) add('FAIL', `owned 漂移 ${ownedRes.drift} 份——装副本手改未跑 sync 刷台账 / 包源改了装副本未同步;须手动同步装副本后跑 node bin/flow-kit.mjs sync 刷台账(见 incidents/2026-09-25-wf-runtime 复盘)`);
232
+ if (ownedRes.drift) add('FAIL', `owned 漂移 ${ownedRes.drift} 份——装副本手改未跑 sync 刷台账 / 包源改了装副本未同步;须手动同步装副本后跑 npx agentic-flow-kit sync 刷台账`);
218
233
  if (ownedRes.gone.length) add('FAIL', `owned 文件缺失:${ownedRes.gone.join('、')}——sync 恢复或手动恢复`);
219
234
  }
220
235
 
221
- // 6.7 跨宿主薄适配正文段漂移校验(2026-09-25 cross-host-sync,参见 workflows/intents/2026-09-25-cross-host-sync.md)
236
+ // 6.6b gate 安装锚校验(2026-10-11 P3-1 gate 防改锚):add-gate 给逐字拷贝安装的 gate 脚本写
237
+ // srcSha256=安装时刻包源 sha(见 src/add-gate.mjs 头注);sync 只按包源刷新模板条目锚,gate 条目
238
+ // 解析不到包源模板(srcTemplatePath→null 提前 return)故锚恒定 → 盘面 sha ≠ 锚即「装后手改」。
239
+ // 设计依据:2026-10-10 调研结论「gate 装户可改、hard 项缺防改」——sync 对 owned 的 sha256 按盘面
240
+ // 自愈使手改不留痕,本项补上可见性。不阻断(沿用「先 WARN 升级 FAIL」渐进路径):装户对门有最终
241
+ // 所有权,锚只提供可见性,有意改动知悉即可。
242
+ const gateRes = checkGateAnchors(target);
243
+ // 出账分级按信任档(2026-10-12 gate-trust-level):project 档 = WARN 永久可见(装户对门有最终
244
+ // 所有权);org 档 = FAIL(doctor exit 1,组织级强制位)。两种档位的**文案主干逐字相同**——
245
+ // 升档只改级别与追加一句「本仓已声明 org 档」,不改判据、不改修复指引(作者照同一套指引修即可)。
246
+ const orgMode = gateRes.trust === 'org';
247
+ const trustNote = orgMode ? '【本仓 kit.json gateTrust=org:组织级强制档,失配属 FAIL】' : '【本仓 kit.json gateTrust=project:项目自治档,失配属 WARN;改 org 可升级为强制】';
248
+ if (gateRes.error) {
249
+ add('WARN', `gate 安装锚校验跳过(kit.json 解析失败:${gateRes.error})`);
250
+ } else if (gateRes.skipped) {
251
+ add('PASS', `gate 安装锚校验跳过(无带锚 gate 条目——旧装户重装 add-gate 后生效)`);
252
+ } else if (gateRes.mismatch.length === 0 && gateRes.gone.length === 0) {
253
+ add('PASS', `gate 安装锚 ${gateRes.total} 份无失配`);
254
+ } else {
255
+ const lv = orgMode ? 'FAIL' : 'WARN';
256
+ if (gateRes.mismatch.length) add(lv, `gate 安装锚失配 ${gateRes.mismatch.length} 份:${gateRes.mismatch.join('、')}——.agents/hooks/ 门禁脚本与安装时包源 sha 不一致(手改会永久留痕)${orgMode ? '(org 档即不可接受)' : ';有意改动知悉即可'}(重装重锚:flow-kit add-gate <门名> --force;恢复原版:从包源 modules/gates/ 拷回)${trustNote}`);
257
+ if (gateRes.gone.length) add(lv, `gate 带锚文件缺失 ${gateRes.gone.length} 份:${gateRes.gone.join('、')}——门禁已不在盘面(失效${orgMode ? ';org 档即不可接受' : ''});恢复:flow-kit add-gate <门名> --force 或从包源 modules/gates/ 拷回${trustNote}`);
258
+ }
259
+
260
+ // 6.7 跨宿主薄适配正文段漂移校验(2026-09-25 cross-host-sync 立法)
222
261
  // 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(.zcode/.omp/.opencode/.trae)下的
223
262
  // {agents,commands}/*.md 是薄适配。按正文段 sha 比对(B-b 语义,frontmatter 不计入漂移),
224
263
  // commands 层按 profiles.mjs#HOSTS.commandPrefix 前缀映射(2026-09-28 起 opencode/trae 同 wf-)。
@@ -230,7 +269,7 @@ export function doctor(args, pkgRoot) {
230
269
  } else if (adapterRes.drift === 0) {
231
270
  add('PASS', `跨宿主薄适配 ${adapterRes.total} 对无正文漂移(装副本引擎 vs 注册表内宿主薄适配,B-b 语义)`);
232
271
  } else {
233
- add('WARN', `跨宿主薄适配正文漂移 ${adapterRes.drift} 对——权威源与薄适配正文段 sha 不一致;按 .agents/commands/sync-hosts.md 跑 flow-kit sync-hosts --apply 单向同步薄适配正文(frontmatter 不动);装户可在 bin/flow-kit.mjs sync 时一并修复`);
272
+ add('WARN', `跨宿主薄适配正文漂移 ${adapterRes.drift} 对——权威源 .agents 与宿主薄适配正文段 sha 不一致。刷新用 flow-kit add-host <宿主> --force(flow-kit sync 不更新宿主适配;sync-hosts 只在包源仓比对 templates/_agents 与 modules/hosts)。`);
234
273
  }
235
274
 
236
275
  // 6.8 workflows 编排脚本 lint(stages 表解析校验:role/step/after 引用、无环、三形态互斥、数值枚举;
@@ -270,6 +309,18 @@ export function doctor(args, pkgRoot) {
270
309
  add('WARN', `模板感知:源仓 ${tplRes.stale.length} 份模板自上次同步后有演进且盘面未跟随:${tplRes.stale.join('、')}——如需跟随从包源 templates/ 拷贝;有意定制可忽略(跑 flow-kit sync 刷新锚后本项静默)`);
271
310
  }
272
311
 
312
+ // 6.10 level-hints 配置体检(2026-10-13 hint-doctor)——非法档位/正则此前只在**跑 flow-kit route 时**
313
+ // 才抛 LEVEL_HINTS_INVALID,而 route 是可选调用 → 写错的清单能静默存活很久。写错配置比没配置危险:
314
+ // 没配置会提示补,写错会误判档位。故给一条主动体检通道。
315
+ // 口径纪律:doctor 不复刻解析器(复刻=判据双源漂移),spawn 装户 level-criteria.mjs 的 --check
316
+ // 入口(判据单源仍是 parseLevelHints 一份)。
317
+ {
318
+ const hr = checkLevelHints(target);
319
+ if (hr.skipped) add('PASS', `level-hints 体检跳过(${hr.note})`);
320
+ else if (hr.ok) add('PASS', `level-hints 配置合法(${hr.note})`);
321
+ else add('FAIL', `level-hints 配置有误——非法档位或正则会让 flow-kit route fail-loud 抛错、按错误下限定级:\n${hr.detail}`);
322
+ }
323
+
273
324
  // 7. check-loop——2026-09-26 check-loop-node 起 node 实现直跑(sh 版曾需先探 sh 可用性:Windows
274
325
  // PowerShell 常无 sh,ENOENT 曾被吞进 hard-block 分支报成空原因假警报,incident 2026-09-24-doctor-sh-enoent;
275
326
  // 迁移后无 sh 依赖,探针退役。check-loop.sh 为兼容 shim,pre-push 钩子路径照常)
@@ -298,6 +349,30 @@ export function doctor(args, pkgRoot) {
298
349
  });
299
350
  }
300
351
 
352
+ // checkLevelHints(2026-10-13 hint-doctor):.agents/level-hints.txt 的**主动**体检。
353
+ // 为什么不能靠 route:非法档位/ERE 此前只在跑 flow-kit route 时才抛 LEVEL_HINTS_INVALID,
354
+ // 而 route 是可选调用 → 写错的清单能静默存活很久,直到某次碰巧路由才炸(写错配置比没配置危险)。
355
+ // 口径纪律:doctor **不复刻解析器**(复刻=判据双源漂移),改为 spawn 装户 .agents/scripts/
356
+ // level-criteria.mjs 的 --check 入口——parseLevelHints 永远只有一份实现。
357
+ // 三态:skipped(无判据单源=1.7.15 前旧装户,不产出告警)/ ok(含未配置=默认空)/ 配置有误。
358
+ export function checkLevelHints(target) {
359
+ const script = path.join(target, '.agents', 'scripts', 'level-criteria.mjs');
360
+ if (!fs.existsSync(script)) return { skipped: true, ok: true, note: '无 .agents/scripts/level-criteria.mjs——1.7.15 之前版本无此判据单源' };
361
+ const r = spawnSync(process.execPath, [script, '--check', path.join(target, '.agents', 'level-hints.txt')], {
362
+ cwd: target, encoding: 'utf8',
363
+ });
364
+ // spawn 本身失败(ENOENT/EACCES/超时被杀)时 status 为 null 且 stderr 可能为空——
365
+ // 不区分会把「体检脚本跑不起来」误报成「配置有误」,排障方向直接被带偏(2026-10-13 executor-review)。
366
+ if (r.error) return { skipped: false, ok: false, detail: `无法执行体检脚本 ${path.basename(script)}:${r.error.message}` };
367
+ if (r.status === 0) return { skipped: false, ok: true, note: String(r.stdout || '').trim() || '通过' };
368
+ if (r.status === null) return { skipped: false, ok: false, detail: `体检脚本异常终止(无退出码,signal=${r.signal || '未知'})——无法判定 level-hints 是否合法` };
369
+ return {
370
+ skipped: false,
371
+ ok: false,
372
+ detail: String(r.stderr || '').split('\n').slice(0, 6).join('\n').trim(),
373
+ };
374
+ }
375
+
301
376
  function print(results) {
302
377
  const icon = { PASS: '✅', WARN: '⚠️ ', FAIL: '❌' };
303
378
  for (const r of results) console.log(`${icon[r.level]} ${r.msg}`);
@@ -364,6 +439,62 @@ export function checkOwnedDrift(target) {
364
439
  return { drift, gone, total: owned.length, skipped: false };
365
440
  }
366
441
 
442
+ // ---- 门禁信任档(2026-10-12 gate-trust-level)----
443
+ // 单源:本文件(doctor §6.6b 判定 + init.mjs 写 kit.json 默认值,两处 import 同一份——
444
+ // 装户侧不消费(kit.json 是包源 CLI 写的台账),故不落 templates/_agents/scripts/policy.mjs:
445
+ // 放那边会让装副本带一个它自己不用的判据,两处真值反而更多。两档语义:
446
+ // 'project'(默认):gate 安装锚失配 = **WARN**。装户对门有最终所有权——锚只提供可见性,
447
+ // 有意改动知悉即可。这是 1.7.11 起的现状,本档只是把隐式默认显式命名为「项目自治」。
448
+ // 'org':失配 = **FAIL**(doctor exit 1)。给多仓/组织场景一个真实的强制位——对齐 Claude Code 的
449
+ // managed policy + allowManagedHooksOnly(组织级强制 + 可屏蔽项目自定义 hook)。
450
+ // 为什么**不把默认改成 org**:本仓诚实边界声明写明「门禁关不掉通道」(台账可写、哈希链防不住整文件
451
+ // 重写)。把可见性升成强制而不同步补上「组织侧锚源的不可篡改性」,等于宣称一个关不掉的通道能关——
452
+ // 那比 WARN 更不可信。默认档与现行为逐字一致(零行为变化),要升的人显式改 kit.json 并自行承担
453
+ // 「组织必须同步管住锚来源」的责任。
454
+ // 非法值 / 缺键(旧台账)→ 回退 'project'(= 零行为变化,**绝不因非法配置变严**)。
455
+ export const GATE_TRUST_LEVELS = ['project', 'org'];
456
+ export const GATE_TRUST_DEFAULT = 'project';
457
+ export function gateTrustOf(kit) {
458
+ const v = kit && kit.gateTrust;
459
+ return GATE_TRUST_LEVELS.includes(v) ? v : GATE_TRUST_DEFAULT;
460
+ }
461
+
462
+ // gate 安装锚校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-10-11 P3-1 gate 防改锚)
463
+ // 判据:kit.owned 中 rel 以 .agents/hooks/ 开头且带 srcSha256 的条目(=add-gate 逐字拷贝安装的 gate 脚本,
464
+ // 锚语义见 src/add-gate.mjs 头注),按 LF 归一算盘面 sha(与 checkOwnedDrift 同口径)比对锚。
465
+ // local-pre-commit 须排除:它是合并接线件,其 srcSha256 由 init/sync 按「模板语义」写(≠ 逐字拷贝锚),
466
+ // 且盘面含各门挂载行——纳入必恒失配误报。
467
+ // 不阻断(默认 'project' 档):装户对门有最终所有权,锚只提供可见性(2026-10-10 调研
468
+ // 「gate 装户可改、hard 项缺防改」结论)。'org' 档(2026-10-12 gate-trust-level,判据单源
469
+ // policy.mjs#gateTrustOf)下失配升 FAIL —— 多仓/组织场景的组织级强制位,对齐 managed policy。
470
+ // 返回:{ total, mismatch, gone, skipped, error?, trust }
471
+ export function checkGateAnchors(target) {
472
+ const kitPath = path.join(target, '.agents', 'kit.json');
473
+ if (!fs.existsSync(kitPath)) return { total: 0, mismatch: [], gone: [], skipped: true };
474
+ let kit;
475
+ try {
476
+ kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
477
+ } catch (e) {
478
+ return { total: 0, mismatch: [], gone: [], skipped: true, error: e.message };
479
+ }
480
+ const trust = gateTrustOf(kit); // 缺键/非法 → 'project'(零行为变化)
481
+ const owned = Array.isArray(kit.owned) ? kit.owned : [];
482
+ const anchored = owned.filter((f) => f.rel.startsWith('.agents/hooks/')
483
+ && f.rel !== '.agents/hooks/local-pre-commit' // 合并接线件,srcSha256 是模板语义锚,非逐字拷贝锚(见上)
484
+ && typeof f.srcSha256 === 'string' && f.srcSha256);
485
+ if (anchored.length === 0) return { total: 0, mismatch: [], gone: [], skipped: true, trust };
486
+ const mismatch = [];
487
+ const gone = [];
488
+ for (const f of anchored) {
489
+ const p = path.join(target, f.rel);
490
+ if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
491
+ // sha 按 LF 归一口径(同 checkOwnedDrift):CRLF 盘面(autocrlf 检出)与锚不误报
492
+ const h = createHash('sha256').update(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8').digest('hex');
493
+ if (h !== f.srcSha256) mismatch.push(f.rel);
494
+ }
495
+ return { total: anchored.length, mismatch, gone, skipped: false, trust };
496
+ }
497
+
367
498
  // 模板下发感知(2026-10-06 template-downstream):只读判据——owned 模板条目三方 sha 比对,
368
499
  // 判定单源 profiles.templateDriftOf(与 sync 出账同源,防两处字面量漂移)。恒不写 kit.json。
369
500
  // 返回 { skipped, note?, total, stale[] }:skipped=true 时 note 说明原因(无 kit / 无 owned /
@@ -418,15 +549,8 @@ export function checkAdapterDrift(target) {
418
549
  return { drift: 0, total: 0, skipped: true, note: '包源环境(templates/ + modules/ 同时存在)——§7.x 仅在装户环境有意义' };
419
550
  }
420
551
  // 4 宿主目录映射(与顶层 HOST_DIR 同源)
421
- const splitFm = (text) => {
422
- const m = String(text || '').match(/^---\r?\n([\s\S]*?\r?\n)---\r?\n?([\s\S]*)$/);
423
- if (!m) return { fm: '', body: String(text || '') };
424
- return { fm: `---\n${m[1]}---\n`, body: m[2] };
425
- };
426
- const bodySha = (p) => {
427
- // 正文段 LF 归一后哈希(closing-coverage:与 sync-hosts bodySha 同口径,CRLF/LF 盘面不误报漂移)
428
- try { return createHash('sha256').update(splitFm(fs.readFileSync(p, 'utf8')).body.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); } catch { return null; }
429
- };
552
+ // splitFm/bodySha 单源复用 sync-hosts(2026-10-11 bodysha-single-source:此前内联第二份实现靠
553
+ // 注释约束同口径,收敛为 import 消灭漂移面)
430
554
  let total = 0;
431
555
  let drift = 0;
432
556
  const listMd = (dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory() ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
@@ -0,0 +1,245 @@
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
+ function loadRouteTable(target, degradations) {
71
+ const p = path.join(target, '.agents', 'commands', 'new-task.md');
72
+ try {
73
+ const m = fs.readFileSync(p, 'utf8').match(ROUTE_TABLE_LINE);
74
+ const table = {};
75
+ for (const part of String(m ? m[1] : '').split('|')) {
76
+ const kv = part.trim().match(/^([a-z]+)\s*=\s*([\w.-]+\.md)$/);
77
+ if (kv) table[kv[1]] = kv[2];
78
+ }
79
+ if (!Object.keys(table).length) throw new Error('route-kinds 行未解析出任何 kind');
80
+ return { table, source: 'new-task.md' };
81
+ } catch (e) {
82
+ 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 后生效` });
83
+ return { table: { ...DEFAULT_ROUTE_TABLE }, source: 'fallback' };
84
+ }
85
+ }
86
+
87
+ // laneOfDoc:调装户 stage-gates.mjs 的单源泳道取法。返回 { key, source, reason }
88
+ // source='stage-gates.mjs' → 真取到单源;source='fallback' → 装户缺该脚本或取不到,按档位直接回落
89
+ // (fail-soft,不阻断路由)。**reason 区分「文件不存在」与「存在但 import 失败」**(2026-10-13
90
+ // executor-review)——两者曾被合并成一句「缺失」,实测把「装户脚本依赖缺失导致 import 报错」诊断成
91
+ // 「未安装」,排障方向直接被带偏。
92
+ async function laneOfDoc(target, level) {
93
+ const p = path.join(target, ...`${AGENT_SCRIPTS_REL}/stage-gates.mjs`.split('/'));
94
+ if (!fs.existsSync(p)) {
95
+ return { key: fallbackLane(level), source: 'fallback', reason: '装户 .agents/scripts/stage-gates.mjs 不存在(未安装或版本更早)' };
96
+ }
97
+ try {
98
+ const mod = await import(pathToFileURL(p).href);
99
+ if (typeof mod.laneOfDoc === 'function') {
100
+ const got = mod.laneOfDoc(level);
101
+ if (got === 'low' || got === 'high' || got === 'suspect') return { key: got, source: 'stage-gates.mjs', reason: '' };
102
+ return { key: fallbackLane(level), source: 'fallback', reason: `装户 stage-gates.mjs 未返回合法泳道值(返回 ${JSON.stringify(got)})` };
103
+ }
104
+ return { key: fallbackLane(level), source: 'fallback', reason: '装户 stage-gates.mjs 未导出 laneOfDoc(版本不匹配)' };
105
+ } catch (e) {
106
+ // 注意:这里必须把真实原因带出去——「脚本在但装不起来」与「脚本不在」是两回事。
107
+ const why = String(e?.message || e).split('\n')[0];
108
+ return { key: fallbackLane(level), source: 'fallback', reason: `装户 stage-gates.mjs 存在但 import 失败:${why}` };
109
+ }
110
+ }
111
+
112
+ const fallbackLane = (level) => ((rankOfLevel(level)) >= 2 ? 'high' : 'low');
113
+ const rankOfLevel = (level) => ({ L0: 0, L1: 1, L2: 2, L3: 3 }[String(level || '').toUpperCase()] ?? 0);
114
+
115
+ export async function route(args) {
116
+ const o = parseArgs(args);
117
+ const target = resolveTarget(o);
118
+ const scriptsDir = path.join(target, ...AGENT_SCRIPTS_REL.split('/'));
119
+ // 降级可见通道(2026-10-13 degradation-channel):此前同一命令里混用四种「缺失」策略——
120
+ // 判据单源缺 → fail-loud;stage-gates 缺 → 静默回落泳道;enums 缺 → 静默用自带表;
121
+ // hints 缺 → 静默按 L1 起步。**只有 laneSource 一个字段对外可见**,--json 消费方拿到的是同一份静默。
122
+ // 口径:fail-loud 的判据单源缺失仍在最前面(改配置比没配置危险),此后**所有软回落一律登记进
123
+ // degradations**,文本与 --json 走同一个通道——降级必须可见,但不因此阻断定级(路由不该因为
124
+ // 词表缺失整个挂掉)。
125
+ const degradations = [];
126
+
127
+ // 判据单源在装户面(随模板下发、随 sync 升级)——包不复制判据,避免双源漂移
128
+ const critPath = path.join(scriptsDir, 'level-criteria.mjs');
129
+ if (!fs.existsSync(critPath)) {
130
+ fail(`未找到 .agents/scripts/level-criteria.mjs(级别判据单源)。\n 该文件随 1.7.15+ 下发;若你的项目更早,执行 flow-kit sync 升级。`);
131
+ }
132
+ const crit = await import(pathToFileURL(critPath).href);
133
+
134
+ // 枚举单源(模块值域 / kind 集合):同样在装户面
135
+ let levelList = crit.LEVELS;
136
+ let modules = [];
137
+ try {
138
+ const enumsMod = await import(pathToFileURL(path.join(scriptsDir, 'workflow-enums.mjs')).href);
139
+ const enums = enumsMod.loadEnums(path.join(scriptsDir, '..', 'workflow-enums.txt'));
140
+ if (Array.isArray(enums['level.all']) && enums['level.all'].length) levelList = enums['level.all'];
141
+ else degradations.push({ code: 'enums-missing-key', detail: '.agents/workflow-enums.txt 无 level.all 键——档位值域回落判据单源自带表' });
142
+ } catch (e) {
143
+ degradations.push({ code: 'enums-fallback', detail: `读不到 .agents/workflow-enums.txt(${String(e?.message || e).split('\n')[0]})——档位值域回落判据单源自带表,不阻断定级` });
144
+ }
145
+ const modFile = path.join(target, '.agents', 'workflow-modules.txt');
146
+ if (fs.existsSync(modFile)) {
147
+ modules = fs.readFileSync(modFile, 'utf8').split(/\r?\n/)
148
+ .map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
149
+ }
150
+
151
+ // 路由表从装户权威 md 读(B5):kind 闭集随之以装户为准,包内不再硬编码 kind 列表
152
+ const route = loadRouteTable(target, degradations);
153
+ const kind = typeof o.kind === 'string' ? o.kind : '';
154
+ if (kind && !Object.keys(route.table).includes(kind)) {
155
+ fail(`--kind 须为 ${Object.keys(route.table).join('|')}(实际「${kind}」,值域读自 ${route.source})`);
156
+ }
157
+
158
+ const files = typeof o.files === 'string'
159
+ ? o.files.split(/[,,]/).map((s) => s.trim()).filter(Boolean)
160
+ : [];
161
+ const proposed = typeof o.level === 'string' ? o.level.toUpperCase() : '';
162
+ if (proposed && !levelList.includes(proposed)) {
163
+ fail(`--level 须为 ${levelList.join('|')}(实际「${proposed}」)`);
164
+ }
165
+
166
+ const a = crit.assessLevel(target, { files, proposed });
167
+ const level = a.level || 'L1';
168
+ // 泳道取法沿装户 stage-gates 单源(laneOfDoc:级别单字段协议),不在本文件复刻 low/high 口径。
169
+ // 装户无 stage-gates.mjs(老版本未升级)→ fail-soft 回退默认(判 L2/L3 → high),并登记降级。
170
+ const laneKey = await laneOfDoc(target, level);
171
+ if (laneKey.source === 'fallback') {
172
+ degradations.push({ code: 'lane-source-fallback', detail: `${laneKey.reason || '装户 .agents/scripts/stage-gates.mjs 缺失或 laneOfDoc 不可用'}——泳道按档位回落判定(建议 flow-kit sync 升级)` });
173
+ }
174
+ if (a.hintsConfigured === false) {
175
+ degradations.push({ code: 'level-hints-empty', detail: `.agents/level-hints.txt 未配置(缺失或为空)→ 机器面给不出档位提示,本次按 ${level} 起步;该清单登记「哪些路径一碰就至少是 L2/L3」(项目私有,只收 L2/L3 两档)` });
176
+ }
177
+ if (a.underHint) {
178
+ degradations.push({ code: 'under-hint-proposed', detail: `建议档 ${a.proposed} 低于路径提示下限 ${a.hintLevel}(命中:${(a.matched || []).slice(0, 4).join('、')})——已按就高不就低取 ${a.hintLevel},建议复核` });
179
+ }
180
+ const lane = laneKey.key === 'high' ? '防御道(同步确认门)' : '协作道(快车道)';
181
+ const entry = route.table[kind] || route.table.require;
182
+
183
+ if (o.json) {
184
+ console.log(JSON.stringify({
185
+ level, lane, laneKey: laneKey.key, laneSource: laneKey.source, kind: kind || '(未定)',
186
+ files, proposed: a.proposed, hintLevel: a.hintLevel, matched: a.matched,
187
+ underHint: a.underHint, hintsConfigured: a.hintsConfigured,
188
+ // 路由表来源(B5):'new-task.md' = 真取到装户机器单源;'fallback' = 包内兜底表(已登记降级)
189
+ routeTableSource: route.source,
190
+ // 降级统一出口(消费方判「这次路由是否在降级态」只需看这一个字段,不必逐个猜 source 字段)
191
+ degraded: degradations.length > 0, degradations,
192
+ nextStage: entry, nextCommand: `.agents/commands/${entry}`,
193
+ modules,
194
+ }, null, 2));
195
+ return;
196
+ }
197
+
198
+ console.log(`flow-kit route — 手动路由判定(机器面)\n`);
199
+ console.log(crit.routeAdvice(level, a));
200
+ console.log('');
201
+ console.log(`泳道:${lane}`);
202
+ if (kind) console.log(`入口指令:.agents/commands/${entry}(kind=${kind})`);
203
+ else console.log(`入口指令:未定 —— 先判 kind(require 新需求 / fix 修复 / verify 验证 / review 评审 / deploy 上线)`);
204
+ console.log(` new-task 阶段索引:.agents/commands/new-task.md`);
205
+ if (degradations.length) {
206
+ console.log('');
207
+ console.log(`⚠️ 降级运行(${degradations.length} 项——机器面能给的部分比完整态少,逐条点名):`);
208
+ for (const d of degradations) console.log(` · [${d.code}] ${d.detail}`);
209
+ }
210
+ console.log('');
211
+ console.log('本命令只给机器可判的部分。**定级仍需语义判断**——按上面自查句逐条判;');
212
+ console.log('不确定就高不就低。要走全自动六阶段闭环另用:flow-kit run start "<需求>"');
213
+ }
214
+
215
+ // 薄壳自有参数:这些是包 CLI 的选项,**不**透传给装户状态机(状态机有自己的参数协议)
216
+ const SHELL_ONLY_KEYS = new Set(['dir']);
217
+
218
+ export function runPipeline(args) {
219
+ const o = parseArgs(args);
220
+ const sub = o._[0];
221
+ // 用法校验先于脚本存在性校验:先告诉用户「怎么用」,再谈装没装(反过来会拿安装问题盖住用法)
222
+ if (!sub) {
223
+ fail('用法:flow-kit run <start|next|status|watch|abort> [...]\n' +
224
+ ' 驱动协议与停机点语义见 .agents/commands/pipeline-run.md(该文件是宿主面权威,本命令只透传)。');
225
+ }
226
+ const target = resolveTarget(o);
227
+ const script = requireInstalledScript(target, `${AGENT_SCRIPTS_REL}/pipeline-run.mjs`);
228
+ // 透传 = 用户给的参数原样交给装户状态机,但**剔除薄壳自有参数**(--dir 是包面寻址,不是状态机参数)
229
+ const passthrough = [];
230
+ for (let i = args.indexOf(sub) + 1; i < args.length; i++) {
231
+ const a = args[i];
232
+ if (a.startsWith('--')) {
233
+ const k = a.slice(2).split('=')[0];
234
+ if (SHELL_ONLY_KEYS.has(k)) {
235
+ if (!a.includes('=') && args[i + 1] !== undefined && !args[i + 1].startsWith('--')) i++;
236
+ continue;
237
+ }
238
+ }
239
+ passthrough.push(a);
240
+ }
241
+ const r = spawnSync(process.execPath, [script, sub, ...passthrough], {
242
+ cwd: target, stdio: 'inherit', windowsHide: true,
243
+ });
244
+ process.exit(r.status ?? 1);
245
+ }