session-orchestrator 3.22.0 → 3.24.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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -38,12 +38,20 @@
38
38
  * it, so nothing turns the YAML into a value.
39
39
  *
40
40
  * S2 exists because S1 alone is fooled by a mention that reads nothing.
41
- * `express-path.enabled` passes S1 on the strength of ONE line —
42
- * `scripts/lib/state-md/body-sections.mjs:699`, a log-message template literal
43
- * that interpolates a value its caller already had. No parser resolves
44
- * `express-path` from config at all; the gate lives entirely in
45
- * `skills/session-start/phase-8-5-express-path.md` prose. S1 called that wired;
46
- * S2 calls it what it is.
41
+ * The exemplar it was written against was `express-path.enabled`: it passed S1 on
42
+ * the strength of ONE line — `scripts/lib/state-md/body-sections.mjs:699`, a
43
+ * log-message template literal interpolating a value its caller already had —
44
+ * while no parser resolved `express-path` from config at all and the gate lived
45
+ * entirely in `skills/session-start/phase-8-5-express-path.md` prose. S1 called
46
+ * that wired; S2 called it what it was.
47
+ *
48
+ * FIXED 2026-08-23 (#1119): `scripts/lib/config.mjs` now parses the block, and
49
+ * `scripts/lib/express-path.mjs` makes the activation decision AND records it.
50
+ * Verified the same day — `express-path` no longer appears in this checker's
51
+ * stdout or stderr under either signal. **The exemplar is therefore historical.**
52
+ * It is kept because it is the clearest statement of what S2 detects, and because
53
+ * a rule that only cites live instances loses its explanation the moment it works.
54
+ * If you need a CURRENT S2 hit, run the checker; do not assume this one.
47
55
  *
48
56
  * S2 applies to TOP-LEVEL keys only — a nested key reaches code through its
49
57
  * parent — and its premise is structural: every Session Config key has to pass
@@ -109,6 +117,29 @@
109
117
  * (`soul-resolve.mjs`, whose only live claim is in `.claude/rules/owner-persona.md`
110
118
  * but whose symbols appear in a 2026-06 changelog entry).
111
119
  *
120
+ * ## S4 `unreachable-library-module` — the question the machine can answer
121
+ *
122
+ * S3 asks a document a question about GRAMMAR and accepts "the prose names an
123
+ * exported symbol" as wiring. That is generous by design, and it is where the
124
+ * largest instance of this defect class hid: `skills/session-start/SKILL.md`
125
+ * Phase 4 named 19 banner probes and their symbols, so S3 read every one of them
126
+ * as wired — while no hook, npm script, CI job or husky stage reached a single
127
+ * one. S1/S2 could not see them either, being config-key checks.
128
+ *
129
+ * S4 drops the grammar question and asks a reachability one: can any process
130
+ * that ACTUALLY STARTS arrive at this file? See `collectUnreachableLibraryModules`
131
+ * for the four conditions, the deliberate CLI-entrypoint boundary (with the
132
+ * measured cost of the alternative), the cluster-root collapse, and the named
133
+ * residuals. S4 does not subsume S3 and is not subsumed by it: S3 catches a
134
+ * REACHABLE module whose document lies about it, S4 catches an unreachable one
135
+ * whose document is honest about what it should do.
136
+ *
137
+ * Output shape is deliberately different from S1-S3 too. Those report a handful
138
+ * of lines; S4 measured 50 on the live tree (2026-08-23 @ 34321bc — a count, so read it as history) of 2026-08-23, which is a BACKLOG.
139
+ * The CLI therefore prints one aggregate WARN carrying the count, and the full
140
+ * census behind `--list` — see `runCheckUnwiredFeatures`. The `findings` array
141
+ * always carries every finding, so no programmatic consumer loses data.
142
+ *
112
143
  * ## Consumer scope, and why "prose-only" is a finding rather than an error
113
144
  *
114
145
  * Read sites are counted in `scripts/**` and `hooks/**` (`.mjs`/`.js`/`.cjs`),
@@ -186,7 +217,18 @@ const CONSUMER_DIRS = Object.freeze(['scripts', 'hooks']);
186
217
  const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
187
218
 
188
219
  /** Directory names excluded from the consumer scan at any depth. */
189
- const EXCLUDED_DIRS = Object.freeze(['node_modules', '.git', 'tests', 'test', '__tests__']);
220
+ // `worktrees` is here for a measured reason, not by analogy to node_modules.
221
+ // This walk uses `readdirSync`, NOT `git ls-files`, so `.gitignore` does not reach
222
+ // it — and `git worktree add` inside the repo (this repo's own convention is
223
+ // `.claude/worktrees/<name>`) drops a COMPLETE second checkout into the walk.
224
+ // Measured 2026-08-23 with one peer worktree present: +755 `.md` and +1209 `.mjs`
225
+ // files, 133 MB, `collectOrphanedProseModules` at 1727 ms of a 2270 ms total.
226
+ // The cost is the smaller half. The correctness half is worse: the peer's copy of
227
+ // `.claude/rules/owner-persona.md` was counted as an INDEPENDENT document naming
228
+ // the same module, so a finding cited two sources where one exists. Any scanner
229
+ // that walks the filesystem instead of the index has this bug; eight under
230
+ // `scripts/lib/validate/` use `readdirSync`.
231
+ const EXCLUDED_DIRS = Object.freeze(['node_modules', '.git', 'tests', 'test', '__tests__', 'worktrees']);
190
232
 
191
233
  /** Extension carrying prose claims (signal S3). */
192
234
  const PROSE_EXTENSIONS = Object.freeze(['.md']);
@@ -220,6 +262,20 @@ const PROSE_EXCLUDED_FILES = Object.freeze(['CHANGELOG.md', 'STATE.md']);
220
262
  */
221
263
  const SELF_REL = path.join('scripts', 'lib', 'validate', 'check-unwired-features.mjs');
222
264
 
265
+ /**
266
+ * Signal S4 entry surfaces: the NON-markdown files that mechanically invoke a
267
+ * module by path. Markdown is deliberately absent — that a SKILL.md names a
268
+ * module is precisely the claim S4 refuses to accept as wiring.
269
+ */
270
+ const WIRING_FILES = Object.freeze(['package.json', '.gitlab-ci.yml']);
271
+
272
+ /** Directory surfaces for S4, as `[dir, extensions]`. `''` catches husky's extensionless stages. */
273
+ const WIRING_DIRS = Object.freeze([
274
+ ['.github', Object.freeze(['.yml', '.yaml'])],
275
+ ['.husky', Object.freeze(['', '.sh'])],
276
+ ['hooks', Object.freeze(['.json', '.sh'])],
277
+ ]);
278
+
223
279
  /**
224
280
  * The config-parser layer: the files a Session Config key must pass through to
225
281
  * become a runtime value. Signal S2 (see header) checks top-level keys against
@@ -233,10 +289,11 @@ const PARSER_PATHS = Object.freeze([
233
289
  ]);
234
290
 
235
291
  /**
236
- * Declared-but-unread keys accepted on purpose. Key = full dotted path,
237
- * value = REASON naming the real consumer. See the header for the contract:
238
- * an empty reason, a key that left every config surface, and a key that got
239
- * wired are all reported so the list stays short and true.
292
+ * Declared-but-unread keys accepted on purpose. Key = full dotted path (S1/S2)
293
+ * or module path relative to the plugin root (S4), value = REASON naming the
294
+ * real consumer. See the header for the contract: an empty reason, a key that
295
+ * left every config surface, and a key that got wired are all reported so the
296
+ * list stays short and true.
240
297
  */
241
298
  const ALLOWLIST = Object.freeze({
242
299
  'auto-skill-dispatch':
@@ -261,7 +318,8 @@ const ALLOWLIST = Object.freeze({
261
318
  /**
262
319
  * @typedef {{
263
320
  * kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'allowlist-missing-reason'
264
- * | 'allowlist-stale' | 'orphaned-prose-module' | 'tool-error',
321
+ * | 'allowlist-stale' | 'orphaned-prose-module' | 'unreachable-library-module'
322
+ * | 'tool-error',
265
323
  * key: string,
266
324
  * message: string,
267
325
  * }} Finding
@@ -542,17 +600,48 @@ export function collectOrphanedProseModules(pluginRoot) {
542
600
  body: readFileSync(absolute, 'utf8'),
543
601
  }));
544
602
 
603
+ // Both membership questions below were nested scans: (1) re-filtered EVERY prose
604
+ // document per module, (2) re-scanned EVERY other module's lines per module.
605
+ // At this repo's size that is 468 x ~700 full-body `includes` plus 468 x 468 x
606
+ // ~300 line tests — measured 1727 ms of `inspectUnwiredFeatures`'s 2270 ms, in a
607
+ // CLI that eight test files spawn under a 30 s hook timeout. Indexing both once
608
+ // is O(corpus) instead of O(corpus^2); the answers are unchanged by construction,
609
+ // and the acceptance criterion for the rewrite was byte-identical CLI output.
610
+ /** basename -> prose docs naming it */
611
+ const claimsByBase = new Map();
612
+ for (const doc of prose) {
613
+ for (const module of modules) {
614
+ if (!doc.body.includes(module.base)) continue;
615
+ const list = claimsByBase.get(module.base);
616
+ if (list) list.push(doc);
617
+ else claimsByBase.set(module.base, [doc]);
618
+ }
619
+ }
620
+ /** basename -> set of module paths referencing it OUTSIDE a comment */
621
+ const referencedBy = new Map();
622
+ for (const other of modules) {
623
+ for (const line of other.lines) {
624
+ if (isCommentLine(line)) continue;
625
+ for (const match of line.matchAll(/[A-Za-z0-9_.-]+\.(?:mjs|js|cjs)/g)) {
626
+ const set = referencedBy.get(match[0]);
627
+ if (set) set.add(other.relative);
628
+ else referencedBy.set(match[0], new Set([other.relative]));
629
+ }
630
+ }
631
+ }
632
+
545
633
  for (const module of modules) {
546
634
  // (1) named by a live document
547
- const claims = prose.filter((doc) => doc.body.includes(module.base));
635
+ const claims = claimsByBase.get(module.base) ?? [];
548
636
  if (claims.length === 0) continue;
549
637
 
550
- // (2) no production module references it outside a comment
551
- const referenced = modules.some(
552
- (other) =>
553
- other.relative !== module.relative &&
554
- other.lines.some((line) => line.includes(module.base) && !isCommentLine(line)),
555
- );
638
+ // (2) no production module references it outside a comment.
639
+ // Self-references do not count, which is why the index stores the referring
640
+ // path rather than a bare boolean.
641
+ const referrers = referencedBy.get(module.base);
642
+ const referenced =
643
+ referrers !== undefined &&
644
+ (referrers.size > 1 || !referrers.has(module.relative));
556
645
  if (referenced) continue;
557
646
 
558
647
  // (3) not invoked by path
@@ -580,6 +669,176 @@ export function collectOrphanedProseModules(pluginRoot) {
580
669
  return { findings, scanned: { modules: modules.length, prose: prose.length } };
581
670
  }
582
671
 
672
+ /**
673
+ * Extract every module-filename token a body mentions, ignoring comment lines.
674
+ *
675
+ * Token extraction beats a substring scan in BOTH directions. It is faster (one
676
+ * pass per file instead of one regex per candidate pair — 468² pair tests on the
677
+ * live tree), and it is more precise: `text.includes('writer.mjs')` is TRUE for
678
+ * `config-writer.mjs`, which silently marks an unrelated module as referenced.
679
+ * The character class stops at `/`, so `'./locks/state-md-lock.mjs'` yields
680
+ * exactly `state-md-lock.mjs`.
681
+ *
682
+ * @param {string[]} lines source lines
683
+ * @returns {Set<string>} module basenames mentioned outside comments
684
+ */
685
+ function mentionedModuleTokens(lines) {
686
+ /** @type {Set<string>} */
687
+ const tokens = new Set();
688
+ for (const line of lines) {
689
+ if (isCommentLine(line)) continue;
690
+ for (const match of line.matchAll(/[A-Za-z0-9_.-]+\.(?:mjs|js|cjs)/g)) tokens.add(match[0]);
691
+ }
692
+ return tokens;
693
+ }
694
+
695
+ /**
696
+ * Signal S4 — library modules no mechanical caller can reach.
697
+ *
698
+ * ## The gap this closes, in one line
699
+ *
700
+ * S3 asks "does a DOCUMENT name a symbol of this module?" and accepts a yes as
701
+ * wiring. S4 asks the question the machine can answer: "can any process that
702
+ * actually starts — a hook, an npm script, a CI job, a husky stage — arrive at
703
+ * this file?" Prose in a SKILL.md is an instruction to an LLM, not a caller: the
704
+ * measured instance is `skills/session-start/SKILL.md` Phase 4, which named 19
705
+ * banner probes that no `.mjs` reached from any entrypoint.
706
+ *
707
+ * ## Entry roots, and why CLI entrypoints are among them
708
+ *
709
+ * Roots are (a) every module named in a NON-markdown wiring surface
710
+ * (`package.json`, `.gitlab-ci.yml`, `.github/workflows/**`, `.husky/**`,
711
+ * `hooks/*.json`) and (b) every CLI entrypoint. Reachability then follows
712
+ * module→module references transitively.
713
+ *
714
+ * (b) is a DELIBERATE boundary, not an oversight. A CLI entrypoint is invoked by
715
+ * path, and "the operator types `/autopilot`, whose skill body runs
716
+ * `node scripts/autopilot.mjs`" IS this repo's architecture — reporting it would
717
+ * indict the design rather than a defect. Measured 2026-08-23: treating CLI
718
+ * entrypoints as non-roots moves the census from 71 to 268 of 468 modules
719
+ * (15.6% → 57.5%), i.e. straight into the broken-instrument band that
720
+ * `.claude/rules/host-resources.md` § HR-101 forbids. A LIBRARY module, by
721
+ * contrast, can only ever be reached by being imported — so "nothing imports it,
722
+ * transitively" is a fact about the machine, not a judgement about prose.
723
+ *
724
+ * ## Cluster roots — one defect, one line
725
+ *
726
+ * Only the ROOT of each unreachable cluster is reported: a module no OTHER
727
+ * unreachable module references. `scripts/lib/owner-config.mjs` has no importer
728
+ * and drags its whole 7-file `owner-config/` subtree down with it; reporting the
729
+ * six interior files would multiply one deletion into seven findings that all
730
+ * disappear together. Measured on the live tree: 71 unreachable modules collapse
731
+ * to 50 roots. This is category separation in the sense of
732
+ * `.claude/rules/development.md` § Guard & Threshold Design — a structural split,
733
+ * never a raised threshold.
734
+ *
735
+ * ## Named residuals
736
+ *
737
+ * - **Basename granularity.** 22 basenames collide across 56 files (7 × `schema.mjs`,
738
+ * 3 × `telemetry.mjs`, …), so a mention of `schema.mjs` marks every `schema.mjs`
739
+ * as referenced. This errs toward WIRED — it can hide a finding, never invent
740
+ * one, which is the right direction for a check whose failure mode is being
741
+ * switched off. Revisit if a real module-resolver (import-specifier resolution
742
+ * relative to the importing file) becomes cheap, or if a collided basename is
743
+ * ever confirmed to mask a true positive.
744
+ * - **Reachable ≠ executed.** A module imported by a hook that never takes that
745
+ * branch reads as wired here. Proving execution needs coverage data, not a graph.
746
+ * - **Reachable from SOME entrypoint is not reachable from the PROMISED one.**
747
+ * This is the sharpest limit and it cost real recall. `ci-status-banner.mjs` is
748
+ * imported by `dispatcher/rank.mjs`, so S4 stays silent — yet CLAUDE.md promises
749
+ * it runs at SESSION-START, and no SessionStart path reached it at `4f6404e`.
750
+ * Same shape for `sessions-integrity-banner.mjs` (via `session-record-repair.mjs`)
751
+ * and `historical-guard.mjs` (via `check-banner-parity.mjs`). Catching those needs
752
+ * a per-entrypoint reachability question ("is X reachable from the SessionStart
753
+ * hook?"), which is a different check with a different root set, not a tightening
754
+ * of this one. Revisit if a second promised-entrypoint claim is ever missed.
755
+ *
756
+ * @param {string} pluginRoot absolute plugin root
757
+ * @returns {{findings: Finding[], scanned: {modules: number, roots: number, unreachable: number}}}
758
+ */
759
+ export function collectUnreachableLibraryModules(pluginRoot) {
760
+ const absolute = CONSUMER_DIRS.flatMap((dir) => walkCode(path.join(pluginRoot, dir))).sort();
761
+ const modules = absolute.map((file) => {
762
+ const body = readFileSync(file, 'utf8');
763
+ const lines = body.split('\n');
764
+ const relative = path.relative(pluginRoot, file);
765
+ return {
766
+ relative,
767
+ base: path.basename(file),
768
+ entrypoint: isCliEntrypoint(body),
769
+ exports: collectExportedSymbols(body),
770
+ // This file contributes NO edges — the S4 counterpart of the SELF_REL
771
+ // exclusion the S1/S2 corpus already applies, and for the identical
772
+ // reason. Every S4 `ALLOWLIST` key is a module path written here as a
773
+ // string literal, so counting it as a mention marks the allowlisted
774
+ // module reachable, S4 stops reporting it, and the entry is then reported
775
+ // `allowlist-stale` — the check silently blinding itself to exactly the
776
+ // module an operator flagged. Measured 2026-08-28 on the first S4
777
+ // allowlist entry: 52 → 51 unreachable modules plus one bogus stale line.
778
+ mentions: relative === SELF_REL ? new Set() : mentionedModuleTokens(lines),
779
+ };
780
+ });
781
+
782
+ // Non-markdown surfaces that mechanically invoke a module by path.
783
+ const wiringBodies = [
784
+ ...WIRING_FILES.map((rel) => path.join(pluginRoot, rel)).filter((file) => existsSync(file)),
785
+ ...WIRING_DIRS.flatMap(([dir, extensions]) =>
786
+ walkCode(path.join(pluginRoot, dir), [], extensions, EXCLUDED_DIRS),
787
+ ),
788
+ ].map((file) => readFileSync(file, 'utf8'));
789
+ const wiringTokens = mentionedModuleTokens(wiringBodies.join('\n').split('\n'));
790
+
791
+ /** @type {Set<string>} */
792
+ const reachable = new Set();
793
+ /** @type {string[]} */
794
+ const stack = [];
795
+ for (const module of modules) {
796
+ if (!module.entrypoint && !wiringTokens.has(module.base)) continue;
797
+ reachable.add(module.relative);
798
+ stack.push(module.relative);
799
+ }
800
+
801
+ const byRelative = new Map(modules.map((module) => [module.relative, module]));
802
+ while (stack.length > 0) {
803
+ const current = byRelative.get(/** @type {string} */ (stack.pop()));
804
+ if (!current) continue;
805
+ for (const module of modules) {
806
+ if (reachable.has(module.relative) || !current.mentions.has(module.base)) continue;
807
+ reachable.add(module.relative);
808
+ stack.push(module.relative);
809
+ }
810
+ }
811
+
812
+ const unreachable = modules.filter(
813
+ (module) => !reachable.has(module.relative) && !module.entrypoint && module.exports.length > 0,
814
+ );
815
+ const unreachableSet = new Set(unreachable.map((module) => module.relative));
816
+ const roots = unreachable.filter(
817
+ (module) =>
818
+ !unreachable.some((other) => other.relative !== module.relative && other.mentions.has(module.base)),
819
+ );
820
+
821
+ const findings = roots.map((module) => {
822
+ const dragged = [...module.mentions].filter(
823
+ (token) => token !== module.base && [...unreachableSet].some((rel) => path.basename(rel) === token),
824
+ );
825
+ const tail = dragged.length > 0 ? `, and drags ${dragged.length} further unreachable module(s)` : '';
826
+ return /** @type {Finding} */ ({
827
+ kind: 'unreachable-library-module',
828
+ key: module.relative,
829
+ message:
830
+ `exports ${module.exports.length} symbol(s) (${module.exports.slice(0, 3).join(', ')}) but no hook, ` +
831
+ `npm script, CI job or husky stage reaches it — transitively${tail}. Only markdown names it, and ` +
832
+ 'prose is an instruction to an LLM, not a caller: wire it, delete it, or allowlist it with a reason',
833
+ });
834
+ });
835
+
836
+ return {
837
+ findings,
838
+ scanned: { modules: modules.length, roots: roots.length, unreachable: unreachable.length },
839
+ };
840
+ }
841
+
583
842
  /**
584
843
  * Run the full census.
585
844
  *
@@ -598,7 +857,14 @@ export function inspectUnwiredFeatures(pluginRoot) {
598
857
  const findings = [];
599
858
  const result = {
600
859
  ok: false,
601
- summary: { declaredKeys: 0, consumerFiles: 0, unwired: 0, allowlisted: 0, orphanedModules: 0 },
860
+ summary: {
861
+ declaredKeys: 0,
862
+ consumerFiles: 0,
863
+ unwired: 0,
864
+ allowlisted: 0,
865
+ orphanedModules: 0,
866
+ unreachableModules: 0,
867
+ },
602
868
  /** @type {string[]} */
603
869
  sourcesScanned: [],
604
870
  findings,
@@ -613,6 +879,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
613
879
  let parserBody;
614
880
  /** @type {ReturnType<typeof collectOrphanedProseModules>} */
615
881
  let orphans;
882
+ /** @type {ReturnType<typeof collectUnreachableLibraryModules>} */
883
+ let unreachable;
616
884
  try {
617
885
  declared = collectDeclaredKeys(pluginRoot);
618
886
  corpus = CONSUMER_DIRS.flatMap((dir) => walkCode(path.join(pluginRoot, dir)))
@@ -630,6 +898,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
630
898
  .map((absolute) => readFileSync(absolute, 'utf8'))
631
899
  .join('\n');
632
900
  orphans = collectOrphanedProseModules(pluginRoot);
901
+ unreachable = collectUnreachableLibraryModules(pluginRoot);
633
902
  } catch (error) {
634
903
  result.toolError = true;
635
904
  findings.push({
@@ -691,6 +960,24 @@ export function inspectUnwiredFeatures(pluginRoot) {
691
960
  findings.push(issue);
692
961
  }
693
962
 
963
+ // S3 — prose promises a module nothing calls. Reported alongside the config
964
+ // census because it is the same defect class one level out: a claim with no
965
+ // mechanism behind it.
966
+ result.summary.orphanedModules = orphans.findings.length;
967
+ findings.push(...orphans.findings);
968
+
969
+ // S4 — no process that actually starts can reach these. Allowlistable on the
970
+ // same terms as a config key: the entry's reason must name the real consumer.
971
+ for (const finding of unreachable.findings) {
972
+ if (Object.prototype.hasOwnProperty.call(ALLOWLIST, finding.key)) {
973
+ result.summary.allowlisted += 1;
974
+ flagged.add(finding.key);
975
+ continue;
976
+ }
977
+ result.summary.unreachableModules += 1;
978
+ findings.push(finding);
979
+ }
980
+
694
981
  for (const key of Object.keys(ALLOWLIST).sort()) {
695
982
  if (flagged.has(key)) continue;
696
983
  findings.push({
@@ -702,12 +989,6 @@ export function inspectUnwiredFeatures(pluginRoot) {
702
989
  });
703
990
  }
704
991
 
705
- // S3 — prose promises a module nothing calls. Reported alongside the config
706
- // census because it is the same defect class one level out: a claim with no
707
- // mechanism behind it.
708
- result.summary.orphanedModules = orphans.findings.length;
709
- findings.push(...orphans.findings);
710
-
711
992
  result.ok = !result.toolError && findings.length === 0;
712
993
  return result;
713
994
  }
@@ -721,7 +1002,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
721
1002
  * @param {string} pluginRoot absolute plugin root
722
1003
  * @returns {number} 0 = scan completed (with or without findings), 2 = tool error
723
1004
  */
724
- export function runCheckUnwiredFeatures(pluginRoot) {
1005
+ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
725
1006
  console.log('--- Check: unwired config keys (declared-but-unread census, WARN-only) ---');
726
1007
  const inspection = inspectUnwiredFeatures(pluginRoot);
727
1008
 
@@ -732,14 +1013,32 @@ export function runCheckUnwiredFeatures(pluginRoot) {
732
1013
  return 2;
733
1014
  }
734
1015
 
735
- const { declaredKeys, consumerFiles, unwired, allowlisted, orphanedModules } = inspection.summary;
1016
+ const { declaredKeys, consumerFiles, unwired, allowlisted, orphanedModules, unreachableModules } =
1017
+ inspection.summary;
1018
+
1019
+ // S4 is a BACKLOG, not a per-run alarm: 50 findings on the live tree against
1020
+ // 1-2 WARN lines from every sibling check. Printing all 50 every run is the
1021
+ // "gate that prints 282 lines gets switched off in week two" failure this
1022
+ // file's own header names. So the default carries the NUMBER (which ratchets,
1023
+ // and which a reviewer can compare run to run) plus the first few paths; the
1024
+ // full census is one `--list` away. Nothing is suppressed — only deferred.
1025
+ const s4 = inspection.findings.filter((item) => item.kind === 'unreachable-library-module');
736
1026
  for (const item of inspection.findings) {
1027
+ if (!list && item.kind === 'unreachable-library-module') continue;
737
1028
  console.log(` WARN: [${item.kind}] ${item.key} — ${item.message}`);
738
1029
  }
1030
+ if (!list && s4.length > 0) {
1031
+ console.log(
1032
+ ` WARN: [unreachable-library-module] ${s4.length} library module(s) that no hook, npm script, ` +
1033
+ `CI job or husky stage can reach — e.g. ${s4.slice(0, 3).map((item) => item.key).join(', ')}. ` +
1034
+ 'Re-run with --list for the full census.',
1035
+ );
1036
+ }
1037
+
739
1038
  console.log(
740
1039
  ` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
741
1040
  `against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
742
- `${orphanedModules} prose-orphaned module(s)`,
1041
+ `${orphanedModules} prose-orphaned module(s), ${unreachableModules} unreachable module(s)`,
743
1042
  );
744
1043
  console.log('');
745
1044
  console.log('Results: 1 passed, 0 failed');
@@ -748,10 +1047,12 @@ export function runCheckUnwiredFeatures(pluginRoot) {
748
1047
 
749
1048
  const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
750
1049
  if (isMain) {
751
- const pluginRoot = process.argv[2];
1050
+ const args = process.argv.slice(2);
1051
+ const pluginRoot = args.find((arg) => !arg.startsWith('-'));
752
1052
  if (!pluginRoot) {
753
- console.error('Usage: check-unwired-features.mjs <plugin-root>');
1053
+ console.error('Usage: check-unwired-features.mjs <plugin-root> [--list]');
1054
+ console.error(' --list print every unreachable-library-module finding instead of the aggregate');
754
1055
  process.exit(2);
755
1056
  }
756
- process.exit(runCheckUnwiredFeatures(path.resolve(pluginRoot)));
1057
+ process.exit(runCheckUnwiredFeatures(path.resolve(pluginRoot), { list: args.includes('--list') }));
757
1058
  }