session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -111,6 +111,7 @@
111
111
  import { readFileSync, readdirSync, statSync } from 'node:fs';
112
112
  import path from 'node:path';
113
113
  import { isMainModule } from '../is-main-module.mjs';
114
+ import { maskSource } from '../js-mask.mjs';
114
115
 
115
116
  /** Only this directory is scanned — a mutation in `scripts/` is production intent. */
116
117
  const SCAN_DIR = 'tests';
@@ -487,43 +488,24 @@ export function insideStringLiteral(body, index) {
487
488
  * "no cwd" measured as a cwd. `inCommentLine` cannot serve here: it answers a
488
489
  * question about a whole LINE, while this one runs inside a call expression.
489
490
  *
491
+ * Delegates to the shared lexer {@link maskSource} in `keepLiterals` mode
492
+ * (#1388). The hand-rolled walker this replaced had no REGEX-LITERAL branch,
493
+ * so a call tail carrying one — measured 2026-09-18:
494
+ * `wrapperHasCwd(", undefined, { env: /\/*$/.source, cwd: dir }")` — read the
495
+ * regex's `/*` as a block-comment opener, swallowed the rest of the tail and
496
+ * returned `false`: a false alarm from this check.
497
+ *
498
+ * One behavioural difference the callers must honour: `maskSource` blanks a
499
+ * comment to SPACES (length-preserving) where this function used to collapse
500
+ * it to a single space. {@link wrapperHasCwd} judges its second positional
501
+ * textually against `second !== ''`, so that difference was checked rather
502
+ * than assumed — see the measurement recorded at that test.
503
+ *
490
504
  * @param {string} text
491
- * @returns {string} same length semantics, comments blanked to one space
505
+ * @returns {string} same length, comment bytes blanked to spaces
492
506
  */
493
507
  export function stripComments(text) {
494
- let out = '';
495
- /** @type {string|null} */
496
- let quote = null;
497
- for (let i = 0; i < text.length; i += 1) {
498
- const ch = text[i];
499
- if (quote !== null) {
500
- out += ch;
501
- if (ch === '\\') {
502
- out += text[i + 1] ?? '';
503
- i += 1;
504
- } else if (ch === quote) quote = null;
505
- continue;
506
- }
507
- if (ch === "'" || ch === '"' || ch === '`') {
508
- quote = ch;
509
- out += ch;
510
- continue;
511
- }
512
- if (ch === '/' && text[i + 1] === '*') {
513
- const end = text.indexOf('*/', i + 2);
514
- i = end === -1 ? text.length : end + 1;
515
- out += ' ';
516
- continue;
517
- }
518
- if (ch === '/' && text[i + 1] === '/') {
519
- const end = text.indexOf('\n', i);
520
- i = end === -1 ? text.length : end - 1;
521
- out += ' ';
522
- continue;
523
- }
524
- out += ch;
525
- }
526
- return out;
508
+ return maskSource(text, { keepLiterals: true });
527
509
  }
528
510
 
529
511
  /**
@@ -549,6 +531,14 @@ export function wrapperHasCwd(tail) {
549
531
  const clean = stripComments(tail);
550
532
  const m = /^\s*,\s*/.exec(clean);
551
533
  if (m) {
534
+ // NO `.trim()` here, and that is measured rather than assumed (#1388): the
535
+ // shared lexer blanks a comment to SPACES where the local stripper used to
536
+ // delete it, so a comment-only tail LOOKS like it would arrive as truthy
537
+ // whitespace. It cannot — the `\s*` above is greedy and consumes every
538
+ // blanked byte, leaving `second === ''`. Measured 2026-09-18 over
539
+ // `, /* no cwd */`, `, /* a */ /* b */`, `,\n // note\n`,
540
+ // `, /* x */ undefined`, `, undefined /* y */`: all `false`, `, /* c */ dir`
541
+ // → `true`. A `.trim()` would be dead code; the case is pinned by a test.
552
542
  const second = clean.slice(m[0].length);
553
543
  if (second !== '' && !/^(?:undefined|null)\b/.test(second) && !/^,/.test(second)) return true;
554
544
  }
@@ -93,7 +93,8 @@
93
93
  * pure input→output date function is out of scope by
94
94
  * construction rather than by exception list.
95
95
  *
96
- * NOT flagged: blocks that control the clock (`now:` arg,
96
+ * NOT flagged: blocks that control the clock (`now:`, `{ now }`
97
+ * or a positional `now` — wider than the seam proof, #1478 —
97
98
  * `vi.useFakeTimers` / `vi.setSystemTime`); date literals in
98
99
  * INPUT position (only `.toBe`/`.toEqual`/`.toStrictEqual`
99
100
  * expected values are read), which leaves the passthrough
@@ -335,18 +336,68 @@ const keyLiteralRe = (key) => new RegExp(`["'\\\\]+${key}["'\\\\]+\\s*:`);
335
336
  * An absolute date pinned as the EXPECTED value of an equality assertion.
336
337
  * Input-position date literals (`created_at: '2026-06-21T…'`) do not match —
337
338
  * that is what keeps the passthrough class (input date === output date) out.
339
+ *
340
+ * CEILING (#1473): a fixed INPUT date that a production TTL ages out later is
341
+ * not flagged either, on purpose. Measured 2026-09-29 with a shim that moves
342
+ * `Date.now()` and `new Date()` forward in every vitest worker: the 34 test
343
+ * files (plus one fixture module) holding a fixed `created_at: '2026-05…09'`
344
+ * (1,211 tests) stayed green at +120, +365 and +3,650 days, so a rule keyed on
345
+ * fixed input dates would have reported false positives only. The one real
346
+ * bomb of that shape, the phase-skip fixture before 6670149c, was green at
347
+ * -60 days and red at +0 and +120 days under the same shim — and
348
+ * scanClockBombs() reaches that file since #1476 (alias imports are read) but
349
+ * stays silent on it: the file hands no clock argument to anything. REVISIT when a
350
+ * second fixed-input-date bomb lands on main: the class then recurs, and a
351
+ * clock-shift run of the suite is the check that finds it without guessing.
338
352
  */
339
353
  const DATE_EXPECTATION =
340
354
  /\.(?:toBe|toEqual|toStrictEqual)\(\s*(['"`])(\d{4}-\d{2}-\d{2}(?:[T ][^'"`]*)?)\1\s*\)/;
341
355
 
342
- /** An explicit clock handed to a callee — the seam this ban asks tests to use. */
343
- const CLOCK_ARG = /\b(?:now|nowMs|nowIso|clock|currentDate)\s*:/;
356
+ /** A clock-named token not read off an object (`Date.now`, `opts.now`) — shared head of the two below. */
357
+ const CLOCK_NAME = String.raw`(?<![.\w$])(?:now|nowMs|nowIso|clock|currentDate)\s*`;
358
+
359
+ /**
360
+ * Pass 1, the seam PROOF: a NAMED clock key (`{ now: … }`) — only that shape says
361
+ * the callee's API takes a clock. Every line it matches also matches
362
+ * CONTROL_CLOCK_ARG, so a block that proves a seam is never flagged itself.
363
+ */
364
+ const SEAM_CLOCK_ARG = new RegExp(`${CLOCK_NAME}:`);
365
+
366
+ /**
367
+ * Pass 2, is this block clock-CONTROLLED: any clock handover. Property shorthand
368
+ * (`{ now }`, `{ repoRoot, now }`) and a positional `now` count too; `Date.now()`
369
+ * and `const now =` do not. Wider is the safe direction here — it can only
370
+ * remove findings.
371
+ *
372
+ * CEILING (#1478): pass 1 reads only the named form, because the wide form also
373
+ * turns helpers into seams — +20 seamed (file, id) pairs in 15 files
374
+ * (172 → 192, measured 2026-10-01 @ ffc5929f), e.g. `expectDeny` via a
375
+ * `{ manifestMtimeMs: now }` fixture. Cost: a file whose ONLY clock handover is
376
+ * `{ now }` or a positional `now` loses its seam proof and its bombs go unseen.
377
+ * REVISIT on the first B5 finding whose subject is a helper, or the first CI
378
+ * `test-value-bans` B5 finding the ±days clock-shift probe shows to be false.
379
+ */
380
+ const CONTROL_CLOCK_ARG = new RegExp(String.raw`${CLOCK_NAME}(?::|,|\}|\))`);
344
381
 
345
382
  /** Freezing the global clock — equally valid control, but not a seam PROOF. */
346
383
  const FAKE_TIMER = /\b(?:useFakeTimers|setSystemTime|advanceTimersByTime|runAllTimers)\b/;
347
384
 
348
- /** `import { a, b as c } from './rel.mjs'` — SUT candidates live behind these. */
349
- const RELATIVE_IMPORT = /import\s+([^;]+?)\s+from\s+['"](\.[^'"]+)['"]/g;
385
+ /**
386
+ * Vitest aliases that resolve to the repo's own modules — source of truth is
387
+ * `resolve.alias` in vitest.config.mjs (today only `@lib`). CEILING: one entry
388
+ * per alias, kept by hand; REVISIT when vitest.config.mjs gains an alias — the
389
+ * parametrised B5 test reads that config and goes red until the key is listed
390
+ * here. Dynamic imports (`await import('@lib/…')`) are not read in either form.
391
+ */
392
+ const ALIAS_PREFIXES = ['@lib/'];
393
+
394
+ /** `import { a, b as c } from './rel.mjs'` or `'@lib/x.mjs'` — SUT candidates live behind these. */
395
+ const LOCAL_IMPORT = new RegExp(
396
+ String.raw`import\s+([^;]+?)\s+from\s+['"]((?:\.|` +
397
+ ALIAS_PREFIXES.map((p) => p.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')).join('|') +
398
+ String.raw`)[^'"]+)['"]`,
399
+ 'g',
400
+ );
350
401
 
351
402
  // ---------------------------------------------------------------------------
352
403
  // File enumeration
@@ -588,17 +639,17 @@ function testBlocks(lines) {
588
639
  }
589
640
 
590
641
  /**
591
- * Identifiers this test file imports from the repo's OWN modules (relative
592
- * specifiers). These are the subject-under-test candidates for B5; framework
593
- * (`vitest`) and stdlib (`node:*`) imports are structurally excluded because
594
- * their specifiers are not relative.
642
+ * Identifiers this test file imports from the repo's OWN modules (relative or
643
+ * vitest-alias specifiers). These are the subject-under-test candidates for B5;
644
+ * framework (`vitest`) and stdlib (`node:*`) imports are structurally excluded
645
+ * because their specifiers match neither form.
595
646
  * @param {string} content
596
647
  * @returns {string[]}
597
648
  */
598
649
  function importedLocalIdentifiers(content) {
599
650
  /** @type {Set<string>} */
600
651
  const ids = new Set();
601
- for (const m of content.matchAll(RELATIVE_IMPORT)) {
652
+ for (const m of content.matchAll(LOCAL_IMPORT)) {
602
653
  const clause = m[1];
603
654
  // `{ a, b as c }` → c ; `x` / `* as ns` → x / ns
604
655
  for (const part of clause.replace(/[{}]/g, ',').split(',')) {
@@ -623,8 +674,9 @@ function callsIdentifier(lines, id) {
623
674
  *
624
675
  * Two passes over the file's `it`/`test` blocks:
625
676
  * 1. PROVE the seam — an imported id called from a block that also hands over
626
- * an explicit clock argument is clock-seamed. A public API only grows a
627
- * `now` parameter because the function reads the clock on its main path.
677
+ * a NAMED clock key (SEAM_CLOCK_ARG) is clock-seamed. A public API only
678
+ * grows a `now` parameter because the function reads the clock on its
679
+ * main path.
628
680
  * 2. FLAG — in blocks with NO clock control at all, any equality assertion
629
681
  * pinning an absolute date against such a subject is a time bomb.
630
682
  *
@@ -648,7 +700,8 @@ function scanClockBombs(relPath, content, lines) {
648
700
  return {
649
701
  start,
650
702
  body,
651
- hasClockArg: live.some((l) => CLOCK_ARG.test(l)),
703
+ provesSeam: live.some((l) => SEAM_CLOCK_ARG.test(l)),
704
+ hasClockArg: live.some((l) => CONTROL_CLOCK_ARG.test(l)),
652
705
  hasFakeTimer: live.some((l) => FAKE_TIMER.test(l)),
653
706
  };
654
707
  });
@@ -656,19 +709,39 @@ function scanClockBombs(relPath, content, lines) {
656
709
  /** @type {Set<string>} */
657
710
  const seamed = new Set();
658
711
  for (const b of blocks) {
659
- if (!b.hasClockArg) continue;
712
+ if (!b.provesSeam) continue;
660
713
  for (const id of sutIds) if (callsIdentifier(b.body, id)) seamed.add(id);
661
714
  }
662
715
  if (seamed.size === 0) return findings;
663
716
 
717
+ // Where an echo may come from: the finding's own block, or the setup every
718
+ // block shares (helpers, constants, beforeEach). Never a sibling test — its
719
+ // assertions, titles and clock literals say nothing about this block's inputs.
720
+ // A block without its own `});` (a one-line it) runs on to the next sibling
721
+ // and takes the setup lines in between out of sharedSetup — that direction can
722
+ // only add a finding, never hide one (1.4% of blocks, measured 2026-09-30).
723
+ /** @type {Set<number>} */
724
+ const inBlock = new Set();
725
+ for (const b of blocks) for (let q = b.start; q < b.start + b.body.length; q++) inBlock.add(q);
726
+ const sharedSetup = lines.filter((_, q) => !inBlock.has(q));
727
+
664
728
  for (const b of blocks) {
665
729
  if (b.hasClockArg || b.hasFakeTimer) continue;
666
730
  const subject = [...seamed].find((id) => callsIdentifier(b.body, id));
667
731
  if (!subject) continue;
732
+ const echoSources = [...b.body, ...sharedSetup].filter(
733
+ (l) => !isCommentLine(l) && !DATE_EXPECTATION.test(l),
734
+ );
668
735
  b.body.forEach((line, k) => {
669
736
  if (isCommentLine(line)) return;
670
737
  const m = DATE_EXPECTATION.exec(line);
671
738
  if (!m) return;
739
+ // Echo: the pinned value is also an input of this block or its shared setup,
740
+ // not a clock reading (the CEILING above keeps fixed INPUT dates unflagged on
741
+ // purpose). CEILING: a real bomb whose date also stands in its own block or
742
+ // the shared setup (its title, a trailing comment, a helper fixture) is
743
+ // swallowed; REVISIT when a clock-shift run finds a bomb this scan missed.
744
+ if (echoSources.some((l) => l.includes(m[2]))) return;
672
745
  findings.push({
673
746
  file: relPath,
674
747
  line: b.start + k + 1,
@@ -204,6 +204,7 @@ const ALLOWED_EMOJI_CODEPOINTS = new Set([
204
204
  0x1f5a5, // 🖥
205
205
  0x1f6a6, // 🚦
206
206
  0x1f6a8, // 🚨
207
+ 0x1f9ed, // 🧭 (navigator banner line in hooks/on-session-start.mjs — #1462)
207
208
  0x2139, // ℹ
208
209
  0x2194, // ↔
209
210
  0x26a0, // ⚠
@@ -108,6 +108,13 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
108
108
  import { spawnSync } from 'node:child_process';
109
109
  import path from 'node:path';
110
110
  import { isMainModule } from '../is-main-module.mjs';
111
+ import { maskSource } from '../js-mask.mjs';
112
+
113
+ // `maskSource` moved to `scripts/lib/js-mask.mjs` (#1388 — three other modules
114
+ // needed the same lexer). `package.json` carries no `exports` map, so every
115
+ // `scripts/lib/**` path is public API: this re-export keeps the OLD import path
116
+ // working for any consumer that already reaches in here.
117
+ export { maskSource } from '../js-mask.mjs';
111
118
 
112
119
  // ---------------------------------------------------------------------------
113
120
  // Constants
@@ -167,108 +174,6 @@ const NON_CALLEES = new Set([
167
174
  'await', 'new', 'do', 'else', 'in', 'of', 'delete', 'void', 'yield', 'import',
168
175
  ]);
169
176
 
170
- // ---------------------------------------------------------------------------
171
- // Source masking — string/template/comment/regex interiors become spaces
172
- // ---------------------------------------------------------------------------
173
-
174
- /**
175
- * Blank out the INTERIOR of every string literal, template literal, comment and
176
- * regex literal, preserving byte offsets and newlines. Delimiters are kept so
177
- * paren/comma balance and identifier boundaries survive.
178
- *
179
- * This is what separates CODE from DATA. A test that embeds fixture source in a
180
- * template literal (this check's own test file does) must not be scanned as
181
- * though that fixture were its own body — masking makes that structural instead
182
- * of relying on a self-exemption marker.
183
- *
184
- * @param {string} text
185
- * @returns {string} same-length masked source
186
- */
187
- export function maskSource(text) {
188
- const out = text.split('');
189
- const n = out.length;
190
- let i = 0;
191
- // Last significant (non-space) char before `i`, used for the regex/division
192
- // disambiguation below.
193
- let prev = '';
194
-
195
- const blank = (from, to) => {
196
- for (let k = from; k < to && k < n; k++) if (out[k] !== '\n') out[k] = ' ';
197
- };
198
-
199
- while (i < n) {
200
- const c = text[i];
201
- const next = text[i + 1];
202
-
203
- // Line comment.
204
- if (c === '/' && next === '/') {
205
- let j = i;
206
- while (j < n && text[j] !== '\n') j++;
207
- blank(i, j);
208
- i = j;
209
- continue;
210
- }
211
- // Block comment.
212
- if (c === '/' && next === '*') {
213
- let j = i + 2;
214
- while (j < n && !(text[j] === '*' && text[j + 1] === '/')) j++;
215
- blank(i, Math.min(j + 2, n));
216
- i = Math.min(j + 2, n);
217
- continue;
218
- }
219
- // Single/double-quoted string — keep the quotes, blank the interior.
220
- if (c === "'" || c === '"') {
221
- let j = i + 1;
222
- while (j < n && text[j] !== c) {
223
- if (text[j] === '\\') j++;
224
- if (text[j] === '\n') break; // unterminated — bail at EOL
225
- j++;
226
- }
227
- blank(i + 1, j);
228
- i = Math.min(j + 1, n);
229
- prev = c;
230
- continue;
231
- }
232
- // Template literal — blank everything including `${…}` substitutions.
233
- if (c === '`') {
234
- let j = i + 1;
235
- while (j < n && text[j] !== '`') {
236
- if (text[j] === '\\') j++;
237
- j++;
238
- }
239
- blank(i + 1, j);
240
- i = Math.min(j + 1, n);
241
- prev = c;
242
- continue;
243
- }
244
- // Regex literal — only when the previous significant char cannot end an
245
- // expression, AND a closing unescaped `/` exists on the SAME line. The
246
- // same-line bound keeps a misread division from swallowing the file.
247
- if (c === '/' && (prev === '' || '(,=:[!&|?{};+-*%~^<>'.includes(prev))) {
248
- let j = i + 1;
249
- let inClass = false;
250
- let closed = -1;
251
- while (j < n && text[j] !== '\n') {
252
- if (text[j] === '\\') { j += 2; continue; }
253
- if (text[j] === '[') inClass = true;
254
- else if (text[j] === ']') inClass = false;
255
- else if (text[j] === '/' && !inClass) { closed = j; break; }
256
- j++;
257
- }
258
- if (closed > i) {
259
- blank(i + 1, closed);
260
- i = closed + 1;
261
- prev = '/';
262
- continue;
263
- }
264
- }
265
-
266
- if (!/\s/.test(c)) prev = c;
267
- i++;
268
- }
269
- return out.join('');
270
- }
271
-
272
177
  // ---------------------------------------------------------------------------
273
178
  // Tiny source scanners (offset-aligned against the masked text)
274
179
  // ---------------------------------------------------------------------------
@@ -53,6 +53,30 @@
53
53
  * a rule that only cites live instances loses its explanation the moment it works.
54
54
  * If you need a CURRENT S2 hit, run the checker; do not assume this one.
55
55
  *
56
+ * S6 `parser-only-config-key` — the key IS read, but the ONLY file that
57
+ * reads it is its own block parser
58
+ * `scripts/lib/config/<root>.mjs`.
59
+ *
60
+ * S6 is the mirror image of S2, and it exists because S1 counts the PARSER as a
61
+ * consumer: `CONSUMER_DIRS` is `scripts/` + `hooks/`, and every block parser
62
+ * lives under `scripts/lib/config/`. A key its parser resolves and nothing else
63
+ * reads is `compact-nudge` wearing a value object. Measured 2026-09-22 on this
64
+ * tree: `reaper.max-hook-latency-ms` and `reaper.false-alarm-window` both passed
65
+ * S1 on `scripts/lib/config/reaper.mjs` alone while `scripts/lib/orphan-reaper.mjs`
66
+ * used same-named HARDCODED defaults and never saw the parsed value.
67
+ *
68
+ * Why the signal is this narrow — one read file, and it is the key's OWN parser.
69
+ * The blunt rule ("the parser layer never counts") was measured first and
70
+ * REJECTED: it flips 102 of 263 declared keys to unwired, because the ordinary
71
+ * shape here is a consumer that reads the camelCase FIELD (`cfg.driftCheck.mode`)
72
+ * while this census matches the kebab TOKEN. 102 of 263 is the broken instrument
73
+ * `.claude/rules/host-resources.md` HR-101 forbids. The narrow rule reports 32 —
74
+ * a backlog, printed as an aggregate like S4 and enumerated under `--list`.
75
+ *
76
+ * S6 carries no camelCase fallback ON PURPOSE: adding one re-hides both reaper
77
+ * keys, whose camelCase names exist in `orphan-reaper/defaults.mjs` as that module's own
78
+ * defaults. A name is not a read.
79
+ *
56
80
  * S2 applies to TOP-LEVEL keys only — a nested key reaches code through its
57
81
  * parent — and its premise is structural: every Session Config key has to pass
58
82
  * through the parser layer to become a value. Measured 2026-08-08: 84 of 89
@@ -393,6 +417,29 @@ const PARSER_PATHS = Object.freeze([
393
417
  path.join('scripts', 'parse-config.mjs'),
394
418
  ]);
395
419
 
420
+ /**
421
+ * The per-block parser directory: `scripts/lib/config/<block>.mjs` turns ONE
422
+ * Session Config block's YAML into a value object and does nothing else.
423
+ *
424
+ * Signal S6 below is about this directory specifically, which is why it is not
425
+ * folded into {@link PARSER_PATHS}.
426
+ */
427
+ const BLOCK_PARSER_DIR = path.join('scripts', 'lib', 'config');
428
+
429
+ /**
430
+ * The key's OWN block parser, by convention `scripts/lib/config/<root>.mjs`.
431
+ *
432
+ * Deliberately an exact name match and not "anything under the parser dir": a
433
+ * sibling block's parser reading the key is still a second file, and S6 only
434
+ * fires when there is exactly ONE reader and it is the key's own.
435
+ *
436
+ * @param {string} root top-level key of the block
437
+ * @returns {string} plugin-root-relative path
438
+ */
439
+ function ownBlockParserPath(root) {
440
+ return path.join(BLOCK_PARSER_DIR, `${root}.mjs`);
441
+ }
442
+
396
443
  /**
397
444
  * Declared-but-unread keys accepted on purpose. Key = full dotted path (S1/S2)
398
445
  * or module path relative to the plugin root (S4), value = REASON naming the
@@ -420,7 +467,8 @@ const ALLOWLIST = Object.freeze({
420
467
 
421
468
  /**
422
469
  * @typedef {{
423
- * kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'allowlist-missing-reason'
470
+ * kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'parser-only-config-key'
471
+ * | 'allowlist-missing-reason'
424
472
  * | 'allowlist-stale' | 'orphaned-prose-module' | 'unreachable-library-module'
425
473
  * | 'coordinator-invoked-module' | 'hand-keyed-learning-subject'
426
474
  * | 'tool-error',
@@ -883,7 +931,9 @@ function mentionedModuleTokens(lines) {
883
931
  * switched off. Revisit if a real module-resolver (import-specifier resolution
884
932
  * relative to the importing file) becomes cheap, or if a collided basename is
885
933
  * ever confirmed to mask a true positive. The `coordinator-invoked-module`
886
- * DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter: in both
934
+ * DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter (since
935
+ * #1298 its predicate also drives the "drags N" count, which had collapsed
936
+ * two dragged `index.mjs` into one): in both
887
937
  * a colliding basename must be named with its `dirname/base` suffix, because
888
938
  * those matches move a module OUT of the reportable class and would otherwise
889
939
  * hide a true unreachable sibling. Measured cost of leaving the root filter
@@ -1014,24 +1064,41 @@ export function collectUnreachableLibraryModules(pluginRoot) {
1014
1064
  const basenameCount = new Map();
1015
1065
  for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
1016
1066
 
1017
- const roots = unreachable.filter((module) => {
1018
- // A bare-basename mention only suppresses when the basename is UNIQUE
1019
- // (see the census above). When it collides, the mentioning module must name
1020
- // the `dirname/base` form in its body — otherwise ONE unreachable module
1021
- // mentioning bare `index.mjs` masks EVERY differently-pathed `index.mjs`
1022
- // root at once (#1293: `locks/index.mjs` + `worktree/index.mjs` were masked
1023
- // by a third unreachable module until that module was deleted for an
1024
- // unrelated reason).
1025
- const ambiguous = (basenameCount.get(module.base) ?? 0) > 1;
1026
- const qualified = module.relative.split(path.sep).slice(-2).join('/');
1027
- const qualifiedRe = ambiguous ? tokenMatcher(qualified) : null;
1028
- return !unreachable.some(
1029
- (other) =>
1030
- other.relative !== module.relative &&
1031
- other.mentions.has(module.base) &&
1032
- (qualifiedRe === null || qualifiedRe.test(other.rawBody)),
1033
- );
1034
- });
1067
+ // One `dirname/base` matcher per unreachable module whose basename collides,
1068
+ // built once: `references` below runs per (mentioner, target) pair.
1069
+ /** @type {Map<string, RegExp>} */
1070
+ const qualifiedMatchers = new Map();
1071
+ for (const module of unreachable) {
1072
+ if ((basenameCount.get(module.base) ?? 0) < 2) continue;
1073
+ qualifiedMatchers.set(module.relative, tokenMatcher(module.relative.split(path.sep).slice(-2).join('/')));
1074
+ }
1075
+ /**
1076
+ * Does `mentioner` reference the unreachable module `target`? A bare-basename
1077
+ * mention only counts when the basename is UNIQUE (see the census above).
1078
+ * When it collides, the mentioning module must name the `dirname/base` form
1079
+ * in its body — otherwise ONE unreachable module mentioning bare `index.mjs`
1080
+ * references EVERY differently-pathed `index.mjs` at once (#1293:
1081
+ * `locks/index.mjs` + `worktree/index.mjs` were masked as roots by a third
1082
+ * unreachable module until that module was deleted for an unrelated reason).
1083
+ *
1084
+ * The ONE predicate for both consumers — the cluster-root filter and the drag
1085
+ * count of the finding message — so the two cannot disagree about who
1086
+ * references whom. They did (#1298): the drag count compared bare basenames,
1087
+ * so a root importing `a/index.mjs` AND `b/index.mjs` read "drags 1".
1088
+ *
1089
+ * @param {(typeof modules)[number]} mentioner
1090
+ * @param {(typeof modules)[number]} target an `unreachable` module
1091
+ * @returns {boolean}
1092
+ */
1093
+ const references = (mentioner, target) => {
1094
+ if (!mentioner.mentions.has(target.base)) return false;
1095
+ const qualifiedRe = qualifiedMatchers.get(target.relative);
1096
+ return qualifiedRe === undefined || qualifiedRe.test(mentioner.rawBody);
1097
+ };
1098
+
1099
+ const roots = unreachable.filter(
1100
+ (module) => !unreachable.some((other) => other.relative !== module.relative && references(other, module)),
1101
+ );
1035
1102
 
1036
1103
  // A pure `export *` barrel enters the population above so it can HEAD its own
1037
1104
  // cluster, but reporting it needs one more condition. S3 already exempts this
@@ -1106,9 +1173,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
1106
1173
  'architecture. Re-check only if that instruction is ever removed',
1107
1174
  });
1108
1175
  }
1109
- const dragged = [...module.mentions].filter(
1110
- (token) => token !== module.base && [...unreachableSet].some((rel) => path.basename(rel) === token),
1111
- );
1176
+ // Counted per MODULE through the root filter's own predicate, never per
1177
+ // basename token: `mentions` is a Set of basenames, so two dragged
1178
+ // `index.mjs` files would collapse into one (#1298).
1179
+ const dragged = unreachable.filter((other) => other.relative !== module.relative && references(module, other));
1112
1180
  const tail = dragged.length > 0 ? `, and drags ${dragged.length} further unreachable module(s)` : '';
1113
1181
  // A pure barrel reports its re-export surface; naming "0 symbol(s)" there
1114
1182
  // would read as a checker bug rather than as the barrel it is.
@@ -1259,6 +1327,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
1259
1327
  orphanedModules: 0,
1260
1328
  unreachableModules: 0,
1261
1329
  coordinatorInvokedModules: 0,
1330
+ parserOnly: 0,
1262
1331
  handKeyedSubjects: 0,
1263
1332
  judgedSubjects: 0,
1264
1333
  },
@@ -1318,7 +1387,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
1318
1387
 
1319
1388
  for (const key of [...declared.keys.keys()].sort()) {
1320
1389
  const meta = /** @type {DeclaredKey} */ (declared.keys.get(key));
1321
- const { code, comment } = countReadSites(meta, corpus);
1390
+ const { code, comment, files: readFiles } = countReadSites(meta, corpus);
1322
1391
 
1323
1392
  /** @type {Finding | null} */
1324
1393
  let issue = null;
@@ -1331,6 +1400,21 @@ export function inspectUnwiredFeatures(pluginRoot) {
1331
1400
  `declared in ${meta.sources.join(' + ')} but no .mjs under ${CONSUMER_DIRS.join('/ or ')}/ ` +
1332
1401
  `reads it${commentNote} — wire it, delete it, or allowlist it with a reason`,
1333
1402
  };
1403
+ } else if (readFiles.length === 1 && readFiles[0] === ownBlockParserPath(meta.root)) {
1404
+ // S6 — the ONLY file that reads this key is the parser that produced it.
1405
+ // A parser is not a consumer: it turns YAML into a field nobody then
1406
+ // reads, which is `compact-nudge` wearing a value object. Measured
1407
+ // 2026-09-22: `reaper.max-hook-latency-ms` and `reaper.false-alarm-window`
1408
+ // both passed S1 on the strength of `scripts/lib/config/reaper.mjs`
1409
+ // alone, while `orphan-reaper.mjs` used its OWN hardcoded defaults of the
1410
+ // same name and never received the parsed value.
1411
+ issue = {
1412
+ kind: 'parser-only-config-key',
1413
+ key,
1414
+ message:
1415
+ `read only by its own block parser (${readFiles[0]}) — a parser is not a consumer; ` +
1416
+ 'nothing outside the config layer reads the parsed value',
1417
+ };
1334
1418
  } else if (key === meta.root && !tokenMatcher(key).test(parserBody)) {
1335
1419
  issue = {
1336
1420
  kind: 'parser-orphan-config-key',
@@ -1356,7 +1440,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
1356
1440
  continue;
1357
1441
  }
1358
1442
 
1359
- result.summary.unwired += 1;
1443
+ if (issue.kind === 'parser-only-config-key') result.summary.parserOnly += 1;
1444
+ else result.summary.unwired += 1;
1360
1445
  findings.push(issue);
1361
1446
  }
1362
1447
 
@@ -1426,6 +1511,7 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
1426
1511
  consumerFiles,
1427
1512
  unwired,
1428
1513
  allowlisted,
1514
+ parserOnly,
1429
1515
  orphanedModules,
1430
1516
  unreachableModules,
1431
1517
  coordinatorInvokedModules,
@@ -1445,8 +1531,16 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
1445
1531
  // architecture — an instruction surface tells an LLM to call the module.
1446
1532
  // Measured 2026-09-07: 46 of the 52 findings the single S4 class carried.
1447
1533
  // Printing 46 WARN lines for the design is the broken instrument HR-101 forbids.
1448
- const DEFERRED = Object.freeze(['unreachable-library-module', 'coordinator-invoked-module']);
1534
+ //
1535
+ // `parser-only-config-key` (S6) is deferred on the S4 terms too: 32 findings
1536
+ // on the live tree is a backlog to work down, not a per-run alarm.
1537
+ const DEFERRED = Object.freeze([
1538
+ 'unreachable-library-module',
1539
+ 'coordinator-invoked-module',
1540
+ 'parser-only-config-key',
1541
+ ]);
1449
1542
  const s4 = inspection.findings.filter((item) => item.kind === 'unreachable-library-module');
1543
+ const s6 = inspection.findings.filter((item) => item.kind === 'parser-only-config-key');
1450
1544
  for (const item of inspection.findings) {
1451
1545
  if (!list && DEFERRED.includes(item.kind)) continue;
1452
1546
  console.log(` WARN: [${item.kind}] ${item.key} — ${item.message}`);
@@ -1465,10 +1559,19 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
1465
1559
  );
1466
1560
  }
1467
1561
 
1562
+ if (!list && s6.length > 0) {
1563
+ console.log(
1564
+ ` WARN: [parser-only-config-key] ${s6.length} declared key(s) whose only reader is their own ` +
1565
+ `block parser — e.g. ${s6.slice(0, 3).map((item) => item.key).join(', ')}. ` +
1566
+ 'Re-run with --list for the full census.',
1567
+ );
1568
+ }
1569
+
1468
1570
  console.log(
1469
1571
  ` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
1470
1572
  `against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
1471
- `${orphanedModules} prose-orphaned module(s), ${unreachableModules} unreachable module(s), ` +
1573
+ `${parserOnly} parser-only, ${orphanedModules} prose-orphaned module(s), ` +
1574
+ `${unreachableModules} unreachable module(s), ` +
1472
1575
  `${coordinatorInvokedModules} coordinator-invoked module(s), ${handKeyedSubjects} hand-keyed ` +
1473
1576
  `learning subject(s) of ${judgedSubjects} judged`,
1474
1577
  );