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
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-validator-registration.mjs — every `scripts/lib/validate/check-*.mjs`
4
+ * must be referenced by basename from at least one of the three surfaces
5
+ * that actually RUN a validator (`scripts/validate-plugin.mjs`,
6
+ * `.husky/pre-commit`, `.gitlab-ci.yml`), or declare itself deliberately
7
+ * standalone. #1184.
8
+ *
9
+ * THE CLASS. A checker with real detection logic and zero callers is built
10
+ * work nobody ever runs — this repo's own `check-unwired-features.mjs`
11
+ * census exists for a sibling shape of the same problem (config keys nobody
12
+ * reads); this checker is that same discipline applied to the checkers
13
+ * directory itself. `scripts/validate-plugin.mjs` is the canonical local
14
+ * runner (`runCheck('check-foo.mjs')`); `.husky/pre-commit` and
15
+ * `.gitlab-ci.yml` are the two OTHER surfaces some checkers wire into
16
+ * directly instead of (or in addition to) the orchestrator —
17
+ * `check-owner-leakage.mjs`, `check-test-fixture-shapes.mjs` and
18
+ * `check-test-value-bans.mjs` all do this (verified live at HEAD 2ccea0f2).
19
+ *
20
+ * OPT-OUT. A checker that is deliberately CLI-only (invoked by a human or a
21
+ * different tool, never by these three surfaces) declares itself with a
22
+ * header-comment marker: `// registration: standalone <reason>`. The marker
23
+ * makes the checker report PASS as "standalone", not merely silence a
24
+ * warning — the same shape of inline self-declared exemption
25
+ * `check-untracked-test-deps.mjs`'s `IGNORE_MARKER` and
26
+ * `check-dead-bridge.mjs`'s `:ignore` marker already use in this directory.
27
+ *
28
+ * ORACLE. A plain substring match of the checker's own basename (e.g.
29
+ * `"check-foo.mjs"`) against the COMMENT-STRIPPED text of the three surface
30
+ * files — the same granularity `runCheck('check-foo.mjs')` calls and
31
+ * `.husky`/CI script lines already use to name a checker. MEASURED (HEAD
32
+ * 2ccea0f2, all 33 live `check-*.mjs` basenames): zero basenames are a
33
+ * substring of another, so this match cannot cross-attribute one checker's
34
+ * registration to a different one.
35
+ *
36
+ * COMMENT-STRIPPING (HIGH, qa review, #1184 FX-C). A basename referenced
37
+ * ONLY inside a `//`/`#` line comment or a `/* *\/` block comment is NOT a
38
+ * real registration — the surface text is stripped of comments (quote-aware,
39
+ * so a `#`/`//` INSIDE a string — a URL fragment, a shell parameter
40
+ * expansion `${VAR#pattern}` — is never mistaken for a comment start) before
41
+ * matching. MEASURED before this fix: a fixture whose only reference to
42
+ * `check-ghost.mjs` sat in `// runCheck('check-ghost.mjs'); // DISABLED`
43
+ * reported `registered: true` (exit 0) — commenting a checker OUT silently
44
+ * kept it PASSing. `scripts/validate-plugin.mjs` uses `//`+`/* *\/` (js);
45
+ * `.husky/pre-commit` and `.gitlab-ci.yml` use `#` (sh/yaml) — see
46
+ * {@link commentStyleForSurface}. The checker's OWN
47
+ * `// registration: standalone` header marker is read from the CHECKER file
48
+ * directly (never from a surface text) and is unaffected by this stripping.
49
+ *
50
+ * NAMED CEILING (BV-004): a checker referenced only in prose (a `.md` doc, a
51
+ * skill body) and nowhere in the three RUN surfaces above still reports
52
+ * UNREGISTERED — being documented is not being run. REVISIT if a fourth run
53
+ * surface (a new CI job file, a different git hook) is ever added: extend
54
+ * `RUN_SURFACES`, do not special-case it here. The comment stripper's own
55
+ * quote-tracking is a single flat state — an escaped quote (`\"`) inside a
56
+ * double-quoted string is not honoured, and a template-literal's `${...}`
57
+ * interpolation is not walked separately. Both failure directions lean
58
+ * toward treating MORE text as "inside a string" than a real parser would,
59
+ * which can only make the stripper MISS a comment (false "still
60
+ * registered"), never manufacture a false UNREGISTERED — the direction this
61
+ * checker's own false-positive history (the paragraph above) already
62
+ * measured as the live hazard.
63
+ *
64
+ * Usage: check-validator-registration.mjs <repo-root>
65
+ * Output: ` PASS: …` / ` FAIL: …` lines (two leading spaces), then
66
+ * `Results: N passed, M failed`. Exit 0 = every checker registered or
67
+ * standalone, 1 = at least one unregistered checker, 2 = tool error.
68
+ *
69
+ * Import-safety: importing this module MUST NOT execute anything — the
70
+ * isMain guard at the bottom is the only side-effecting path.
71
+ */
72
+
73
+ import { existsSync, readFileSync } from 'node:fs';
74
+ import path from 'node:path';
75
+ import { pathToFileURL } from 'node:url';
76
+ import { listRepoFiles } from './repo-files.mjs';
77
+
78
+ /** Marker line inside a checker's own header — declares deliberate CLI-only status. */
79
+ export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
80
+
81
+ /** The three surfaces that actually RUN a validator (not merely mention it). */
82
+ const RUN_SURFACES = Object.freeze([
83
+ path.join('scripts', 'validate-plugin.mjs'),
84
+ path.join('.husky', 'pre-commit'),
85
+ path.join('.gitlab-ci.yml'),
86
+ ]);
87
+
88
+ const VALIDATE_DIR_REL = path.join('scripts', 'lib', 'validate');
89
+
90
+ /**
91
+ * Comment style for a RUN_SURFACES path, keyed on extension rather than
92
+ * position — avoids a silent index-drift if `RUN_SURFACES` is ever
93
+ * reordered. `.mjs` gets JS-shaped comments (`//`, `/* *\/`, quotes
94
+ * `'`/`"`/`` ` ``); everything else (`.husky/pre-commit` has no extension,
95
+ * `.gitlab-ci.yml`) gets shell/YAML-shaped comments (`#` only, quotes
96
+ * `'`/`"`).
97
+ *
98
+ * @param {string} rel repo-relative surface path
99
+ * @returns {{lineComment: string, blockComment: boolean, quoteChars: string[]}}
100
+ */
101
+ function commentStyleForSurface(rel) {
102
+ return rel.endsWith('.mjs')
103
+ ? { lineComment: '//', blockComment: true, quoteChars: ['"', "'", '`'] }
104
+ : { lineComment: '#', blockComment: false, quoteChars: ['"', "'"] };
105
+ }
106
+
107
+ /**
108
+ * Strip comments from `text` so a checker basename mentioned only inside a
109
+ * comment is never read as a real registration. Quote-aware: walks `'`/`"`
110
+ * (and, for the js style, `` ` ``) spans without inspecting their contents,
111
+ * so a comment marker INSIDE a string (a URL fragment `#frag`, a shell
112
+ * parameter expansion `${VAR#pattern}`) is left untouched rather than
113
+ * truncating the line early. See the header NAMED CEILING for what this
114
+ * quote-tracking deliberately does not attempt.
115
+ *
116
+ * @param {string} text
117
+ * @param {{lineComment: string, blockComment: boolean, quoteChars: string[]}} style
118
+ * @returns {string}
119
+ */
120
+ export function stripComments(text, { lineComment, blockComment, quoteChars }) {
121
+ let out = '';
122
+ let i = 0;
123
+ let inQuote = null;
124
+ while (i < text.length) {
125
+ const ch = text[i];
126
+ if (inQuote) {
127
+ out += ch;
128
+ if (ch === '\\' && i + 1 < text.length) {
129
+ out += text[i + 1];
130
+ i += 2;
131
+ continue;
132
+ }
133
+ if (ch === inQuote) inQuote = null;
134
+ i += 1;
135
+ continue;
136
+ }
137
+ if (quoteChars.includes(ch)) {
138
+ inQuote = ch;
139
+ out += ch;
140
+ i += 1;
141
+ continue;
142
+ }
143
+ if (blockComment && ch === '/' && text[i + 1] === '*') {
144
+ const end = text.indexOf('*/', i + 2);
145
+ i = end === -1 ? text.length : end + 2;
146
+ continue;
147
+ }
148
+ if (text.startsWith(lineComment, i)) {
149
+ const nl = text.indexOf('\n', i);
150
+ i = nl === -1 ? text.length : nl; // keep the newline itself, drop the comment text
151
+ continue;
152
+ }
153
+ out += ch;
154
+ i += 1;
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /**
160
+ * @typedef {{basename: string, registered: boolean, standalone: boolean, surfaces: string[]}} RegistrationResult
161
+ */
162
+
163
+ /**
164
+ * @param {string} repoRoot
165
+ * @returns {RegistrationResult[]} sorted by basename
166
+ */
167
+ export function scanValidatorRegistration(repoRoot) {
168
+ const checkerFiles = listRepoFiles(repoRoot, { dirs: [VALIDATE_DIR_REL], exts: ['mjs'] }).filter(
169
+ (f) => /^check-.*\.mjs$/.test(path.basename(f)),
170
+ );
171
+
172
+ // Comment-stripped before matching (HIGH, #1184 FX-C): a basename
173
+ // referenced only inside a `//`/`#`/`/* *\/` comment is NOT a real
174
+ // registration — see the header's COMMENT-STRIPPING paragraph.
175
+ const surfaceTexts = RUN_SURFACES.map((rel) => {
176
+ const abs = path.join(repoRoot, rel);
177
+ try {
178
+ const raw = existsSync(abs) ? readFileSync(abs, 'utf8') : '';
179
+ return stripComments(raw, commentStyleForSurface(rel));
180
+ } catch {
181
+ return '';
182
+ }
183
+ });
184
+
185
+ /** @type {RegistrationResult[]} */
186
+ const results = [];
187
+ for (const abs of checkerFiles) {
188
+ const basename = path.basename(abs);
189
+ let source = '';
190
+ try {
191
+ source = readFileSync(abs, 'utf8');
192
+ } catch {
193
+ /* unreadable — no marker can be found, no surface reference can save it either */
194
+ }
195
+ const standalone = STANDALONE_MARKER.test(source);
196
+ const surfaces = RUN_SURFACES.filter((_, i) => surfaceTexts[i].includes(basename));
197
+ results.push({ basename, registered: surfaces.length > 0, standalone, surfaces });
198
+ }
199
+ return results.sort((a, b) => a.basename.localeCompare(b.basename));
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // CLI
204
+ // ---------------------------------------------------------------------------
205
+
206
+ /**
207
+ * Run the check against a repo root, printing the validate-plugin line
208
+ * vocabulary.
209
+ *
210
+ * @param {string} repoRoot
211
+ * @returns {number} 0 = every checker registered or standalone, 1 = finding(s), 2 = tool error
212
+ */
213
+ export function runCheckValidatorRegistration(repoRoot) {
214
+ console.log('--- Check: validator registration (check-*.mjs must be wired or standalone) ---');
215
+
216
+ const results = scanValidatorRegistration(repoRoot);
217
+ let pass = 0;
218
+ let fail = 0;
219
+
220
+ for (const r of results) {
221
+ if (r.standalone) {
222
+ console.log(` PASS: ${r.basename} — declared standalone (registration: standalone)`);
223
+ pass += 1;
224
+ } else if (r.registered) {
225
+ console.log(` PASS: ${r.basename} — referenced in ${r.surfaces.join(', ')}`);
226
+ pass += 1;
227
+ } else {
228
+ console.log(
229
+ ` FAIL: ${r.basename} — referenced by NEITHER scripts/validate-plugin.mjs, .husky/pre-commit, NOR .gitlab-ci.yml, and carries no "registration: standalone" marker`,
230
+ );
231
+ fail += 1;
232
+ }
233
+ }
234
+
235
+ console.log('');
236
+ console.log(`Results: ${pass} passed, ${fail} failed`);
237
+ return fail > 0 ? 1 : 0;
238
+ }
239
+
240
+ const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
241
+ if (isMain) {
242
+ const root = process.argv[2];
243
+ if (!root) {
244
+ console.error('Usage: check-validator-registration.mjs <repo-root>');
245
+ process.exit(2);
246
+ }
247
+ process.exit(runCheckValidatorRegistration(path.resolve(root)));
248
+ }
@@ -194,6 +194,7 @@
194
194
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
195
195
  import path from 'node:path';
196
196
  import { pathToFileURL } from 'node:url';
197
+ import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
197
198
 
198
199
  /** Directories whose content is scanned. Root-level `*.md` is added separately. */
199
200
  const SCAN_DIRS = Object.freeze([
@@ -224,9 +225,6 @@ const DOC_EXTENSIONS = Object.freeze(['.md']);
224
225
  /** Code extensions (comment + string-literal rules apply). */
225
226
  const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
226
227
 
227
- /** Fence languages whose body is shell. Anything else is prose. */
228
- const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
229
-
230
228
  /**
231
229
  * Real top-level subcommands, harvested from `gh --help` / `glab --help`
232
230
  * (gh 2.86.0 / glab 1.91.0, 2026-08-14). Filter 4: a token that is not in this
@@ -576,33 +574,13 @@ export function scanMarkdown(relative, body, tally) {
576
574
  const findings = [];
577
575
  /** @type {{line: number, text: string}[]} */
578
576
  const shellLines = [];
579
- /** @type {{marker: string, length: number, shell: boolean} | null} */
580
- let fence = null;
581
577
 
582
- const lines = body.split('\n');
583
- for (let index = 0; index < lines.length; index += 1) {
584
- const raw = lines[index];
585
- const fenceMatch = raw.match(/^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/);
586
- if (fenceMatch) {
587
- const marker = fenceMatch[1][0];
588
- const length = fenceMatch[1].length;
589
- const lang = fenceMatch[2].toLowerCase();
590
- if (fence === null) {
591
- fence = { marker, length, shell: SHELL_LANGS.has(lang) };
592
- continue;
593
- }
594
- // A closing fence uses the same char, is at least as long, and has no info string.
595
- if (marker === fence.marker && length >= fence.length && lang === '') {
596
- fence = null;
597
- continue;
598
- }
599
- // Otherwise it is fence content (a nested fence inside a wider one).
600
- }
601
- if (fence === null || !fence.shell) continue;
578
+ forEachLine(body, (raw, { lineNumber, inFence, lang }) => {
579
+ if (!inFence || !SHELL_LANGS.has(lang)) return;
602
580
  const stripped = raw.replace(/^\s*[$❯>]\s+/, '');
603
- if (/^\s*#/.test(stripped)) continue;
604
- shellLines.push({ line: index + 1, text: stripped });
605
- }
581
+ if (/^\s*#/.test(stripped)) return;
582
+ shellLines.push({ line: lineNumber, text: stripped });
583
+ });
606
584
 
607
585
  for (const entry of joinContinuations(shellLines)) {
608
586
  for (const hit of extractBareInvocations(entry.text, tally)) {
@@ -0,0 +1,196 @@
1
+ /**
2
+ * markdown-fences.mjs — the ONE fenced-code-block tracker shared by every
3
+ * Markdown-scanning validator/extractor in this repo (#1181).
4
+ *
5
+ * ## Why one module
6
+ *
7
+ * The same regex pair + open/close comparison had drifted into four separate
8
+ * copies — `check-doc-cli-commands.mjs`, `check-skill-script-paths.mjs`,
9
+ * `check-vcs-repo-flag.mjs` and `auq/parse.mjs` — each tracking fence depth
10
+ * and language independently. A drifted copy is a silent one: nothing fails
11
+ * when copy #3 diverges from copy #1, because each copy only has to agree
12
+ * with itself.
13
+ *
14
+ * ## The rule this module encodes
15
+ *
16
+ * A fence opens on a line beginning with 3+ backticks or 3+ tildes, carrying
17
+ * an optional info string (most commonly a language tag). It closes on a
18
+ * line whose marker CHARACTER matches, whose LENGTH is at least the
19
+ * opener's, and whose info string is empty — CommonMark reserves the info
20
+ * string for the OPENING fence only, so a fence-shaped line that still
21
+ * carries one is fence CONTENT (most often a nested fence one level in), not
22
+ * a closer.
23
+ *
24
+ * ## The one real divergence between the four original copies, preserved as a parameter
25
+ *
26
+ * Three of the four copies anchor the fence-line regex only at the START of
27
+ * the line — trailing text after the info string is simply not captured,
28
+ * never rejected. `auq/parse.mjs`'s copy anchors at BOTH ends: a line
29
+ * carrying anything past the info string besides trailing whitespace is not
30
+ * recognised as a fence line at all. This is `{ wholeLine: true }` below —
31
+ * a real behavioural difference, not stylistic, so it stays a caller-chosen
32
+ * option rather than being silently resolved one way. Every other
33
+ * consumer-specific behaviour (the shell-language predicate, blockquote
34
+ * stripping, unbalanced-fence reporting) likewise stays at the call site —
35
+ * this module owns only the fence-line grammar itself.
36
+ */
37
+
38
+ /** Fence languages whose body is shell. Both `check-doc-cli-commands.mjs` and
39
+ * `check-vcs-repo-flag.mjs` filtered on this identical literal set before
40
+ * extraction; centralised here rather than kept as two copies of the same
41
+ * five strings. */
42
+ export const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
43
+
44
+ /** Start-anchored: matches CommonMark, tolerates trailing info-string text. */
45
+ const FENCE_LINE_START_RE = /^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/;
46
+
47
+ /** Whole-line-anchored: nothing but whitespace may follow the info string. */
48
+ const FENCE_LINE_WHOLE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*([^\s`~]*)[ \t]*$/u;
49
+
50
+ /**
51
+ * Match a candidate fence-marker line.
52
+ *
53
+ * @param {string} line
54
+ * @param {{ wholeLine?: boolean }} [options] `wholeLine: true` requires the
55
+ * ENTIRE line (after the marker) to be nothing but the info string plus
56
+ * trailing whitespace — the stricter reading `auq/parse.mjs` needs.
57
+ * Default `false` only anchors the START of the line, matching
58
+ * CommonMark and the three `check-*.mjs` validators.
59
+ * @returns {{ marker: string, length: number, info: string } | null}
60
+ */
61
+ export function matchFenceLine(line, { wholeLine = false } = {}) {
62
+ const match = (wholeLine ? FENCE_LINE_WHOLE_RE : FENCE_LINE_START_RE).exec(line);
63
+ if (!match) return null;
64
+ return { marker: match[1][0], length: match[1].length, info: match[2] ?? '' };
65
+ }
66
+
67
+ /**
68
+ * Does `candidate` close the fence opened by `open`? Same marker character,
69
+ * at least as long, and carrying no info string of its own.
70
+ *
71
+ * @param {{ marker: string, length: number }} open
72
+ * @param {{ marker: string, length: number, info: string }} candidate
73
+ * @returns {boolean}
74
+ */
75
+ export function closesFence(open, candidate) {
76
+ return candidate.marker === open.marker && candidate.length >= open.length && candidate.info === '';
77
+ }
78
+
79
+ /**
80
+ * Normalise a fence's info string into a comparable language tag. Every
81
+ * caller that classified a fence by language did `.toLowerCase()` (never
82
+ * `.trim()`, since the capturing regex cannot include leading/trailing
83
+ * whitespace in the first place) before comparing against a language set —
84
+ * this makes that normalisation a single, explicit step instead of an
85
+ * implicit property of the extraction regex.
86
+ *
87
+ * @param {string} info
88
+ * @returns {string}
89
+ */
90
+ export function normalizeLang(info) {
91
+ return String(info ?? '').trim().toLowerCase();
92
+ }
93
+
94
+ /**
95
+ * Strip a leading blockquote `>` chain so a quoted fence (`> \`\`\``) is
96
+ * still recognised as a fence line. Only `check-skill-script-paths.mjs`
97
+ * needs this — a fence inside a blockquote is still a fence there, but the
98
+ * other callers never scan quoted content.
99
+ *
100
+ * @param {string} line
101
+ * @returns {string}
102
+ */
103
+ export function stripBlockquote(line) {
104
+ return line.replace(/^(?:\s*>)+\s?/, '');
105
+ }
106
+
107
+ /**
108
+ * Walk `text` line by line, tracking fence state, and call `onLine` for
109
+ * every CONTENT line. Fence-marker lines themselves (the opener and the
110
+ * closer) are consumed by the tracker and never handed to the callback —
111
+ * all four original copies treated marker lines as structural, never as
112
+ * scannable content. A fence-shaped line that neither opens nor validly
113
+ * closes (a nested fence one level in) IS still content and reaches the
114
+ * callback, exactly as in the original copies.
115
+ *
116
+ * @param {string} text
117
+ * @param {(line: string, state: { lineNumber: number, inFence: boolean, lang: string | null }) => void} onLine
118
+ * @param {{ wholeLine?: boolean, stripBlockquotes?: boolean }} [options]
119
+ * @returns {{ unbalancedFenceLine: number | null }} the 1-based line of a
120
+ * fence that opened and never closed by EOF, or `null` if every fence
121
+ * this walk saw was balanced.
122
+ */
123
+ export function forEachLine(text, onLine, options = {}) {
124
+ const { wholeLine = false, stripBlockquotes = false } = options;
125
+ const lines = text.split('\n');
126
+ /** @type {{ marker: string, length: number, openLine: number } | null} */
127
+ let fence = null;
128
+ let lang = null;
129
+
130
+ for (let index = 0; index < lines.length; index += 1) {
131
+ const raw = lines[index];
132
+ const probe = stripBlockquotes ? stripBlockquote(raw) : raw;
133
+ const candidate = matchFenceLine(probe, { wholeLine });
134
+ if (candidate) {
135
+ if (fence === null) {
136
+ fence = { marker: candidate.marker, length: candidate.length, openLine: index + 1 };
137
+ lang = normalizeLang(candidate.info);
138
+ continue;
139
+ }
140
+ if (closesFence(fence, candidate)) {
141
+ fence = null;
142
+ lang = null;
143
+ continue;
144
+ }
145
+ // Otherwise it is fence content (a nested fence inside a wider one) — falls through.
146
+ }
147
+ onLine(raw, { lineNumber: index + 1, inFence: fence !== null, lang });
148
+ }
149
+
150
+ return { unbalancedFenceLine: fence ? fence.openLine : null };
151
+ }
152
+
153
+ /**
154
+ * Extract fence BLOCKS (an open/close pair plus its body) rather than a
155
+ * per-line walk — the shape `auq/parse.mjs` needs, since it iterates fences
156
+ * as units, not lines.
157
+ *
158
+ * An unbalanced fence (opens, never closes) contributes NO block, mirroring
159
+ * the original `fencesOf()` contract: it silently drops the dangling opener
160
+ * rather than reporting it. Unlike `check-skill-script-paths.mjs`, whose
161
+ * unterminated fence is a doc DEFECT worth its own finding, `auq/parse.mjs`
162
+ * scans AskUserQuestion blocks in production code and prose, where an
163
+ * unterminated fence was never treated as a reportable condition in its own
164
+ * right — only as "no block found here".
165
+ *
166
+ * @param {string} text
167
+ * @param {{ wholeLine?: boolean }} [options]
168
+ * @returns {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>}
169
+ */
170
+ export function scanFenceBlocks(text, options = {}) {
171
+ const { wholeLine = false } = options;
172
+ const lines = text.split('\n');
173
+ /** @type {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>} */
174
+ const blocks = [];
175
+ /** @type {{ marker: string, length: number, info: string, startIdx: number } | null} */
176
+ let open = null;
177
+
178
+ for (let index = 0; index < lines.length; index += 1) {
179
+ const candidate = matchFenceLine(lines[index], { wholeLine });
180
+ if (!candidate) continue;
181
+ if (open === null) {
182
+ open = { ...candidate, startIdx: index };
183
+ continue;
184
+ }
185
+ if (!closesFence(open, candidate)) continue; // fence content (nested), not a close
186
+ blocks.push({
187
+ openLine: open.startIdx + 1,
188
+ closeLine: index + 1,
189
+ lang: open.info,
190
+ bodyLines: lines.slice(open.startIdx + 1, index),
191
+ bodyStartLine: open.startIdx + 2,
192
+ });
193
+ open = null;
194
+ }
195
+ return blocks;
196
+ }