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
@@ -12,18 +12,12 @@
12
12
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
 
15
- // The pi projection is IMPORTED, not copied: pi-hook-bridge.mjs is the runtime
16
- // that actually rewrites `hook_event_name` for every Pi event, so it is the one
17
- // source of truth for which pi event becomes which Claude event (#953). A local
18
- // literal here was a second copy of the same 5 pairs — it could drift silently,
19
- // and the validator would then certify a projection the runtime does not use.
20
- // Deliberately NOT the same for `cursorEventMap` below: nothing at runtime
21
- // projects Cursor events (Cursor calls the handlers directly), so the
22
- // validator's copy is the only witness there. Even if a runtime source existed,
23
- // reading Check 3's expectation from the artefact it checks would make the
24
- // check self-certifying — the "undocumented pi-native event" test at
25
- // tests/scripts/check-hooks-symmetry.test.mjs would become unreachable. Keep
26
- // cursorEventMap validator-owned; do not "helpfully" collapse it too.
15
+ // The pi and cursor projections are IMPORTED, not copied: the runtime bridges
16
+ // rewrite `hook_event_name` for every native event, so they are the source of
17
+ // truth for which native event becomes which Claude event (#953). A local
18
+ // literal here was a second copy — it could drift silently, and the validator
19
+ // would then certify a projection the runtime does not use.
20
+ import { CURSOR_TO_CANONICAL_EVENT } from '../cursor-hook-bridge.mjs';
27
21
  import { PI_TO_CANONICAL_EVENT } from '../pi-hook-bridge.mjs';
28
22
 
29
23
  const PLUGIN_ROOT = process.argv[2];
@@ -40,7 +34,19 @@ const DOCUMENTED_ASYMMETRIES = {
40
34
  // Events from hooks.json/hooks-codex.json that are intentionally absent in hooks-cursor.json
41
35
  cursorMissingFromMain: ['SessionStart', 'SessionEnd', 'PostToolUse', 'PostToolUseFailure', 'PostToolBatch', 'Stop', 'SubagentStart', 'SubagentStop', 'CwdChanged', 'PreToolUse'],
42
36
  // Events unique to hooks-cursor.json (Cursor IDE-specific)
43
- cursorOnly: ['afterFileEdit', 'beforeShellExecution'],
37
+ cursorOnly: [
38
+ 'afterFileEdit',
39
+ 'afterShellExecution',
40
+ 'beforeShellExecution',
41
+ 'postToolUse',
42
+ 'postToolUseFailure',
43
+ 'preToolUse',
44
+ 'sessionEnd',
45
+ 'sessionStart',
46
+ 'stop',
47
+ 'subagentStart',
48
+ 'subagentStop',
49
+ ],
44
50
  // Claude/Codex events with no Pi-native v1 mapping yet.
45
51
  piMissingFromMain: ['PostToolUseFailure', 'PostToolBatch', 'SubagentStart', 'SubagentStop', 'CwdChanged'],
46
52
  // Pi-native extension events that map onto Claude/Codex hook events.
@@ -48,23 +54,10 @@ const DOCUMENTED_ASYMMETRIES = {
48
54
  // validator checks hooks-pi.json against is the SAME object pi-hook-bridge.mjs
49
55
  // applies to live payloads, so the two cannot drift apart.
50
56
  piEventMap: PI_TO_CANONICAL_EVENT,
51
- // Cursor-IDE-native events projected onto the logical Claude events they
52
- // correspond to. WITHOUT this projection Check 6 compared a FOREIGN event
53
- // namespace against hooks.json: not one key matched, the per-event loop
54
- // `continue`d on every iteration, and hooks-cursor.json reported
55
- // "handler sets match hooks.json (documented asymmetries: 0)" while missing
56
- // 20 of 22 handlers — a vacuum-true PASS. Check 6's projection guard now
57
- // fails closed PER declared event (#946), so dropping a single entry from
58
- // this map is caught too, not only the loss of every projection: the first
59
- // version tested `sharedEvents.length === 0`, under which removing just
60
- // `afterFileEdit` left the run green and merely moved cursor's documented-
61
- // asymmetry count from 12 to 8.
62
- // afterFileEdit fires AFTER the edit (hooks-cursor.json `note`,
63
- // docs/cursor-setup.md) → it projects onto PostToolUse, never PreToolUse.
64
- cursorEventMap: {
65
- beforeShellExecution: 'PreToolUse',
66
- afterFileEdit: 'PostToolUse',
67
- },
57
+ // Bound to the runtime constant (see the import comment): the projection this
58
+ // validator checks hooks-cursor.json against is the SAME object
59
+ // cursor-hook-bridge.mjs applies to live payloads (#919 closed).
60
+ cursorEventMap: CURSOR_TO_CANONICAL_EVENT,
68
61
  // Check 6 (#942): handlers wired on a Claude event but intentionally absent
69
62
  // from the SAME logical event on a counterpart manifest. Checks 1-3 compare
70
63
  // event KEYS and Check 4 handler EXISTENCE — a handler wired on only one
@@ -159,21 +152,9 @@ const DOCUMENTED_ASYMMETRIES = {
159
152
  'pre-bash-issue-budget.mjs', // #946
160
153
  ],
161
154
  },
162
- // Cursor is NOT wired (#919) and will not be: hooks-cursor.json is a
163
- // mapping REFERENCE, not live enforcement. Every handler expects Claude
164
- // Code payload shapes (tool_name === 'Bash', tool_input.command) and emits
165
- // a Claude PreToolUse envelope; fed a Cursor payload enforce-commands.mjs
166
- // short-circuits at gate G1 and writes 0 bytes to stdout AND stderr with
167
- // exit 0, so the harness sees no decision and the command runs. Cursor
168
- // needs an input/output adapter like scripts/lib/pi-hook-bridge.mjs; none
169
- // exists. Operator decision 2026-07-31 — gap registered, not closed:
170
- // #919 (Cursor no-op) tracks the adapter, #946 the allowlist-provenance
171
- // rule that every entry here names its issue. These entries exist so the
172
- // gap is MACHINE-readable (Check 6 counts them) rather than prose-only.
155
+ // Cursor is wired through scripts/lib/cursor-hook-bridge.mjs (#919 closed).
156
+ // Remaining gaps are tools Cursor does not expose, not missing adapters.
173
157
  cursor: {
174
- // #919: beforeShellExecution → PreToolUse. Only enforce-commands.mjs is
175
- // mapped at all; the other eight PreToolUse handlers have no Cursor
176
- // mapping whatsoever.
177
158
  PreToolUse: [
178
159
  // pre-task-scope-disjoint (#1020): NOT ported by construction. The hook
179
160
  // matches the `Agent` dispatch tool, which this platform does not have —
@@ -191,24 +172,9 @@ const DOCUMENTED_ASYMMETRIES = {
191
172
  // tests/hooks/pre-auq-clarity-wiring.test.mjs, which goes red then.
192
173
  'pre-auq-clarity.mjs',
193
174
  'pre-task-scope-disjoint.mjs',
194
- 'skill-invocation-telemetry.mjs', // #919
195
- 'enforce-scope.mjs', // #919
196
- 'config-protection.mjs', // #919
197
- 'pre-bash-destructive-guard.mjs', // #919
198
- 'pre-bash-staging-fence.mjs', // #919
199
- 'pre-bash-memory-propose-audit.mjs', // #919
200
- 'pre-bash-sessions-ledger-guard.mjs',// #919 (#958 — needs tool_name === 'Bash')
201
- 'pre-bash-templates-first.mjs', // #919
202
- 'pre-bash-issue-budget.mjs', // #919
203
- ],
204
- // #919: afterFileEdit → PostToolUse. Cursor maps enforce-scope.mjs here
205
- // (post-hoc warning only); none of Claude's four PostToolUse handlers
206
- // has a Cursor mapping.
207
- PostToolUse: [
208
- 'post-edit-validate.mjs', // #919
209
- 'post-tooluse-frontend-slop.mjs', // #919
210
- 'post-bash-write-verify.mjs', // #919 (#942 class — needs tool_name === 'Bash')
211
- 'loop-guard.mjs', // #919
175
+ // skill-invocation-telemetry: Cursor has no Skill tool — the matcher
176
+ // can never fire. Commands Read skills/*/SKILL.md instead.
177
+ 'skill-invocation-telemetry.mjs',
212
178
  ],
213
179
  },
214
180
  },
@@ -358,7 +324,7 @@ function extractHandlersByEvent(json) {
358
324
  const hookList = m.hooks || [];
359
325
  for (const h of hookList) {
360
326
  const cmd = h.command || h.script || '';
361
- const match = cmd.match(/(?:hooks\/|\$\{?CLAUDE_PLUGIN_ROOT\}?\/hooks\/|\$\{?CODEX_PLUGIN_ROOT\}?\/hooks\/|\$\{?PI_PLUGIN_ROOT\}?\/hooks\/)([\w/-]+\.mjs)/);
327
+ const match = cmd.match(/(?:hooks\/|\$\{?CLAUDE_PLUGIN_ROOT\}?\/hooks\/|\$\{?CODEX_PLUGIN_ROOT\}?\/hooks\/|\$\{?PI_PLUGIN_ROOT\}?\/hooks\/|\$\{?CURSOR_PLUGIN_ROOT\}?\/hooks\/)([\w/-]+\.mjs)/);
362
328
  if (match) handlers.add(match[1]);
363
329
  }
364
330
  }
@@ -36,8 +36,9 @@
36
36
  * - Verify non-scanned file extensions (e.g. .yaml) are not processed
37
37
  */
38
38
 
39
- import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
39
+ import { readFileSync, existsSync } from 'node:fs';
40
40
  import { join, relative } from 'node:path';
41
+ import { listRepoFiles } from './repo-files.mjs';
41
42
 
42
43
  const pluginRoot = process.argv[2];
43
44
  if (!pluginRoot) {
@@ -86,26 +87,6 @@ const SCAN_ROOTS = [
86
87
 
87
88
  const SCAN_EXTENSIONS = ['.md', '.mjs', '.js', '.ts'];
88
89
 
89
- /**
90
- * Recursively walk a directory and return all file paths with the allowed extensions.
91
- * @param {string} dir
92
- * @returns {string[]}
93
- */
94
- function walk(dir) {
95
- const results = [];
96
- if (!existsSync(dir)) return results;
97
- for (const name of readdirSync(dir)) {
98
- const full = join(dir, name);
99
- const st = statSync(full);
100
- if (st.isDirectory()) {
101
- results.push(...walk(full));
102
- } else if (SCAN_EXTENSIONS.some((ext) => name.endsWith(ext))) {
103
- results.push(full);
104
- }
105
- }
106
- return results;
107
- }
108
-
109
90
  const violations = [];
110
91
 
111
92
  for (const rel of SCAN_ROOTS) {
@@ -114,7 +95,17 @@ for (const rel of SCAN_ROOTS) {
114
95
  pass(`${rel}/ does not exist yet (no scan needed)`);
115
96
  continue;
116
97
  }
117
- const files = walk(absDir);
98
+ // The index, not the filesystem (#1143). This walk carried NO exclusion set
99
+ // — not even `node_modules` — while SCAN_EXTENSIONS covers .mjs/.js/.ts, so
100
+ // a vendored copy of the very package this canary forbids (a gitignored
101
+ // `skills/playwright-driver/node_modules/@playwright/mcp`) would be read as
102
+ // repo source and fail a BLOCKING gate on someone else's code.
103
+ // Honest scope note: unlike the other scanners migrated with it, this one is
104
+ // NOT exposed to the `.claude/worktrees/` case — measured 2026-08-26, its
105
+ // three SCAN_ROOTS are fixed deep subdirectories that a worktree cannot land
106
+ // in, and on-disk vs index counts were identical (2/2, 3/3, 2/2). The
107
+ // gitignored-vendored-tree case above is the reason it moved.
108
+ const files = listRepoFiles(pluginRoot, { dirs: [rel], exts: SCAN_EXTENSIONS });
118
109
  let fileViolations = 0;
119
110
  for (const file of files) {
120
111
  const text = readFileSync(file, 'utf8');
@@ -13,7 +13,8 @@
13
13
  * 7. Each name is unique within the array
14
14
  * 8. Array contains an entry named "ecosystem-health"
15
15
  * 9. Array contains an entry named "convergence-monitor"
16
- * 10. The 2 referenced watcher scripts exist on filesystem
16
+ * 10. Array contains an entry named "wave-transcript-tail" (#1114)
17
+ * 11. The 3 referenced watcher scripts exist on filesystem
17
18
  *
18
19
  * Path resolution note:
19
20
  * experimental.monitors value "./monitors/monitors.json" is repo-root-relative
@@ -50,11 +51,16 @@ let failed = 0;
50
51
  function pass(msg) { console.log(` PASS: ${msg}`); passed += 1; }
51
52
  function fail(msg) { console.log(` FAIL: ${msg}`); failed += 1; }
52
53
 
53
- const REQUIRED_MONITOR_NAMES = ['ecosystem-health', 'convergence-monitor'];
54
+ const REQUIRED_MONITOR_NAMES = [
55
+ 'ecosystem-health',
56
+ 'convergence-monitor',
57
+ 'wave-transcript-tail',
58
+ ];
54
59
  const REQUIRED_FIELDS = ['name', 'command', 'description'];
55
60
  const REQUIRED_SCRIPTS = [
56
61
  'scripts/lib/ecosystem-health.mjs',
57
62
  'scripts/lib/convergence-monitor.mjs',
63
+ 'scripts/lib/wave-transcript-tail.mjs',
58
64
  ];
59
65
 
60
66
  // ---------------------------------------------------------------------------
@@ -177,7 +183,7 @@ if (dups.length === 0) {
177
183
  }
178
184
 
179
185
  // ---------------------------------------------------------------------------
180
- // Checks 8-9: required monitor names present
186
+ // Checks 8-10: required monitor names present
181
187
  // ---------------------------------------------------------------------------
182
188
 
183
189
  for (const required of REQUIRED_MONITOR_NAMES) {
@@ -189,7 +195,7 @@ for (const required of REQUIRED_MONITOR_NAMES) {
189
195
  }
190
196
 
191
197
  // ---------------------------------------------------------------------------
192
- // Check 10: referenced watcher scripts exist on disk
198
+ // Check 11: referenced watcher scripts exist on disk
193
199
  // ---------------------------------------------------------------------------
194
200
 
195
201
  for (const scriptRel of REQUIRED_SCRIPTS) {
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Check: every `scripts/**.mjs` path cited in `skills/`, `commands/` and
4
+ * `agents/` either EXISTS or is annotated as deliberately absent (#1176).
5
+ * Extended (#1187) to also cite `scripts/**.sh` and `hooks/**.sh` — see
6
+ * "## Mode: BLOCKING for `.mjs`, ADVISORY for `.sh`" below for why that half
7
+ * is advisory, not blocking.
8
+ *
9
+ * ## Why
10
+ *
11
+ * Prose is not executed. A skill body that tells the coordinator to run
12
+ * `node scripts/lib/auto-commit.mjs` costs an operator a failed command and a
13
+ * re-derivation of what the file was supposed to do — and nothing in the
14
+ * corpus notices, because a markdown file compiles under every gate. Measured
15
+ * 2026-09-02 @ c3ab480: 237 distinct citations across the three scan roots,
16
+ * 7 of them dead.
17
+ *
18
+ * ## Fences are skipped, and that is most of the answer
19
+ *
20
+ * 4 of those 7 sat inside fenced code blocks — synthetic example paths
21
+ * (`scripts/example.mjs`, `scripts/lib/a.mjs`) in a snippet demonstrating a
22
+ * command's argument shape. A fenced snippet is an illustration of a FORM, not
23
+ * a claim that a file exists, so the shared fence tracker
24
+ * (`./markdown-fences.mjs`, #1181) silences them structurally
25
+ * rather than by allowlist.
26
+ *
27
+ * ## Annotation, and why placement is a rule rather than a convenience
28
+ *
29
+ * A citation in PROSE is a claim. When the claim is deliberate — a planned file
30
+ * behind an issue, a historical path kept for narrative, an inline example —
31
+ * say so on the line:
32
+ *
33
+ * <!-- path-check: planned #214 -->
34
+ * <!-- path-check: historical -->
35
+ * <!-- path-check: example -->
36
+ *
37
+ * The marker is honoured on the SAME line as the citation, or on the line
38
+ * IMMEDIATELY above WHEN THAT LINE CITES NOTHING ITSELF — nowhere else. A line
39
+ * carrying `citation + marker` exempts only that citation; it does not reach
40
+ * down to the next line, which would silently exempt a citation nobody
41
+ * annotated. Two lines above is INERT and the citation
42
+ * still reports, which is pinned by a test. The reason is the rule
43
+ * `recurring-issue-an-exemption-marker-that-only-works-same-line-is-visually-identical-to-one-in-a-comment-block-3bff005.md`
44
+ * in `.claude/rules/`:
45
+ * a marker that reads like an exemption but changes nothing is worse than no
46
+ * marker at all, because the guard then looks wrong instead of the marker
47
+ * looking misplaced. A malformed marker (unknown class, or `planned` without a
48
+ * `#<iid>`) is itself a finding for the same reason — it must never fail silent.
49
+ *
50
+ * ## Mode: BLOCKING for `.mjs`, ADVISORY for `.sh` (#1187)
51
+ *
52
+ * Unlike `check-doc-cli-commands.mjs`, the oracle here is the repository's own
53
+ * filesystem, not a locally installed third-party binary — there is no version
54
+ * skew that could red an unrelated commit. So `.mjs` findings are `FAIL:` and
55
+ * the check returns non-zero, EXACTLY as before this module grew a second
56
+ * extension.
57
+ *
58
+ * The `.sh` half of the citation grammar (below) does not get that same
59
+ * severity by default. A #1176 repo-wide grep (`scripts/hooks` prose across
60
+ * `skills/commands/agents/docs/hooks`) found 27 distinct `.sh` citations, 21
61
+ * dead — but only ONE of those 27 sits inside this checker's three scan roots
62
+ * (`skills/contract-version-bump/SKILL.md:134`, itself arguably a
63
+ * cross-repo path — see the dry-run note at `scanSkillScriptPaths`'s
64
+ * `strictSh` option). The other 26 live in `docs/`, which this checker does
65
+ * NOT scan and — per this same paragraph's own evidence — MUST NOT start
66
+ * scanning as a side effect of the `.sh` extension: `docs/adr/*.md` alone
67
+ * carries 7 dead `.mjs` citations of its own (all historical/planned ADR
68
+ * prose, e.g. `scripts/lib/tool-adapter.mjs`, `scripts/lib/auto-commit.mjs`),
69
+ * none annotated, all outside this task's edit scope. Widening `SCAN_DIRS` to
70
+ * `docs` would turn those 7 into new BLOCKING findings on a doc surface
71
+ * nobody triaged — the opposite of "the `.mjs` behaviour stays exactly as
72
+ * today". So `SCAN_DIRS` stays `['skills', 'commands', 'agents']`; the wider
73
+ * `docs`/`hooks` prose census is a follow-up for whoever owns those files,
74
+ * not a silent scope change here.
75
+ *
76
+ * A `.sh` finding is therefore `WARN:` by default (visible, never blocking —
77
+ * `ok` and the CLI exit code ignore `severity: 'warn'` findings) and only
78
+ * becomes `FAIL:`/blocking under the `--strict-sh` CLI flag (or
79
+ * `strictSh: true` for `scanSkillScriptPaths()` callers) — flip that default
80
+ * once the dead `.sh` citations this checker CAN see are fixed by their doc
81
+ * owner (BV-004 revisit trigger).
82
+ *
83
+ * @module scripts/lib/validate/check-skill-script-paths
84
+ */
85
+
86
+ import { existsSync, readFileSync } from 'node:fs';
87
+ import path from 'node:path';
88
+ import { pathToFileURL } from 'node:url';
89
+ import { listRepoFiles } from './repo-files.mjs';
90
+ import { forEachLine } from './markdown-fences.mjs';
91
+
92
+ /** Documentation roots whose prose is treated as a claim about the repo. */
93
+ export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents']);
94
+
95
+ /**
96
+ * A cited script path. One regex, one alternation, reused for every
97
+ * extension rather than a second scanner (#1187): `scripts/**.mjs` (the
98
+ * original, still the only `.mjs` root scanned), `scripts/**.sh` and
99
+ * `hooks/**.sh`. `hooks/**.mjs` is deliberately NOT part of this grammar —
100
+ * the `.mjs` half of the citation surface stays exactly `scripts/`, matching
101
+ * every existing annotation and fence-skip test unchanged.
102
+ */
103
+ const CITATION_RE = /scripts\/[a-zA-Z0-9_/-]*\.(?:mjs|sh)|hooks\/[a-zA-Z0-9_/-]*\.sh/g;
104
+
105
+ /**
106
+ * Filename fragments that mark a citation as an ILLUSTRATIVE placeholder —
107
+ * `scripts/example.sh`, `hooks/my-hook.sh`, `scripts/<name>.sh` — rather than
108
+ * a claim that a real file exists. Checked only for a citation that already
109
+ * failed `existsSync` (a real file is never suppressed by this list, no
110
+ * matter what it's named). Recognised automatically, with no marker needed,
111
+ * because #1176 found 6 such `hooks/*.mjs` example names in hook-development
112
+ * prose (`hooks/example.mjs`, `guard.mjs`, `my-hook.mjs`, …) that would
113
+ * otherwise all need a hand-written `<!-- path-check: example -->` on every
114
+ * occurrence.
115
+ *
116
+ * Ceiling (BV-004): exactly these six fragments, case-insensitive substring
117
+ * match. A REAL path that happens to contain one of them (`scripts/lib/
118
+ * foobar-report.mjs`, `hooks/my-guard.sh`) is indistinguishable from a
119
+ * placeholder by this heuristic and would be silently swallowed if it were
120
+ * ever cited before being created. Revisit by shrinking this list (never
121
+ * growing it further) the moment that collision is observed for real — the
122
+ * escape hatch until then is the same `<!-- path-check: planned #<iid> -->`
123
+ * marker every other deliberate citation already uses.
124
+ */
125
+ const PLACEHOLDER_FRAGMENTS = Object.freeze(['example', 'my-', '<', 'placeholder', 'foo', 'bar']);
126
+
127
+ /**
128
+ * Is `citedPath` an illustrative placeholder name rather than a real path?
129
+ *
130
+ * @param {string} citedPath
131
+ * @returns {boolean}
132
+ */
133
+ export function isPlaceholderCitation(citedPath) {
134
+ const lower = citedPath.toLowerCase();
135
+ return PLACEHOLDER_FRAGMENTS.some((fragment) => lower.includes(fragment));
136
+ }
137
+
138
+ /** The annotation marker, in any of its three classes. */
139
+ const ANNOTATION_RE = /<!--\s*path-check:\s*([^>]*?)\s*-->/;
140
+
141
+ /**
142
+ * Judge one annotation payload.
143
+ *
144
+ * @param {string} payload the text between `path-check:` and `-->`
145
+ * @returns {{ok: boolean, class: string}}
146
+ */
147
+ export function classifyAnnotation(payload) {
148
+ const text = payload.trim();
149
+ if (text === 'historical' || text === 'example') return { ok: true, class: text };
150
+ const planned = text.match(/^planned\s+#(\d+)$/);
151
+ if (planned) return { ok: true, class: `planned #${planned[1]}` };
152
+ return { ok: false, class: text };
153
+ }
154
+
155
+ /**
156
+ * Split a markdown body into citations and annotations, both OUTSIDE fences.
157
+ *
158
+ * The fence automaton is `./markdown-fences.mjs` (#1181 — one tracker
159
+ * shared with `check-doc-cli-commands.mjs` and
160
+ * `check-vcs-repo-flag.mjs`): a fence opens on ``` / ~~~ with an optional
161
+ * info string and closes on the same character, at least as long, with no
162
+ * info string.
163
+ *
164
+ * Two properties are load-bearing because the automaton fails OPEN:
165
+ *
166
+ * 1. A fence that never closes swallows the whole rest of the file. That is a
167
+ * doc defect in its own right (`unbalanced-fence`), so it is REPORTED —
168
+ * and the swallowed tail is re-read as prose, so a dead citation hiding
169
+ * behind the unmatched opener still surfaces instead of being silenced by
170
+ * the very defect that made it invisible. Measured on
171
+ * `agents/db-specialist.md`, where a stray closing fence opened a block
172
+ * that ran to EOF and blinded the last 41 lines.
173
+ * 2. A fence inside a blockquote (`> ```) is a fence. Without stripping the
174
+ * `>` chain first, a quoted fenced example is read as prose and its
175
+ * illustrative paths are reported — a false red, the fail-CLOSED mirror of
176
+ * the same blind spot.
177
+ *
178
+ * @param {string[]} lines body split on `\n`
179
+ * @returns {{citations: {line: number, path: string}[], annotations: Map<number, {ok: boolean, class: string, raw: string}>, unbalancedFence: {line: number} | null}}
180
+ */
181
+ export function extractCitations(lines) {
182
+ /** @type {{line: number, path: string}[]} */
183
+ const citations = [];
184
+ /** @type {Map<number, {ok: boolean, class: string, raw: string}>} */
185
+ const annotations = new Map();
186
+
187
+ /**
188
+ * Read one line as prose.
189
+ *
190
+ * @param {string} raw the line
191
+ * @param {number} lineNumber its 1-based position
192
+ */
193
+ const collect = (raw, lineNumber) => {
194
+ const annotation = raw.match(ANNOTATION_RE);
195
+ if (annotation) {
196
+ annotations.set(lineNumber, { ...classifyAnnotation(annotation[1]), raw: annotation[0] });
197
+ }
198
+ for (const hit of raw.matchAll(CITATION_RE)) {
199
+ citations.push({ line: lineNumber, path: hit[0] });
200
+ }
201
+ };
202
+
203
+ // A blockquoted fence is still a fence — the shared tracker strips the `>`
204
+ // chain before detection so the quoted example's body stays fenced.
205
+ const { unbalancedFenceLine } = forEachLine(
206
+ lines.join('\n'),
207
+ (raw, { lineNumber, inFence }) => {
208
+ if (inFence) return;
209
+ collect(raw, lineNumber);
210
+ },
211
+ { stripBlockquotes: true },
212
+ );
213
+
214
+ if (unbalancedFenceLine === null) return { citations, annotations, unbalancedFence: null };
215
+
216
+ // EOF with the fence still open: never swallow silently. Re-read the tail as
217
+ // prose so the citations the defect hid are reported alongside it.
218
+ for (let index = unbalancedFenceLine; index < lines.length; index += 1) collect(lines[index], index + 1);
219
+ return { citations, annotations, unbalancedFence: { line: unbalancedFenceLine } };
220
+ }
221
+
222
+ /**
223
+ * Census the documentation corpus for dead `scripts/**.mjs`/`.sh` and
224
+ * `hooks/**.sh` citations.
225
+ *
226
+ * @param {{pluginRoot: string, dirs?: string[], strictSh?: boolean}} options
227
+ * `strictSh` (default `false`) promotes a dead `.sh` citation from
228
+ * `severity: 'warn'` to `severity: 'fail'` — see the module docblock
229
+ * "Mode: BLOCKING for `.mjs`, ADVISORY for `.sh`" for why the default stays
230
+ * advisory in this release.
231
+ * @returns {{ok: boolean, summary: object, findings: {kind: string, file: string, line: number, path: string, annotation: string | null, message: string, severity: 'fail' | 'warn'}[], toolError: boolean}}
232
+ */
233
+ export function scanSkillScriptPaths({ pluginRoot, dirs = SCAN_DIRS, strictSh = false }) {
234
+ /** @type {{kind: string, file: string, line: number, path: string, annotation: string | null, message: string, severity: 'fail' | 'warn'}[]} */
235
+ const findings = [];
236
+ const summary = {
237
+ filesScanned: 0,
238
+ citations: 0,
239
+ existing: 0,
240
+ annotated: 0,
241
+ placeholders: 0,
242
+ findings: 0,
243
+ warnings: 0,
244
+ };
245
+
246
+ /** @type {string[]} */
247
+ let files;
248
+ try {
249
+ // The git index, never a `readdirSync` walk (#1143): a walk cannot see
250
+ // `.gitignore`, so a worktree under `.claude/worktrees/` or any ignored
251
+ // artefact would enter this census as if it were repository documentation.
252
+ files = listRepoFiles(pluginRoot, { dirs, exts: ['.md'] });
253
+ } catch (error) {
254
+ findings.push({
255
+ kind: 'tool-error',
256
+ file: '-',
257
+ line: 0,
258
+ path: '-',
259
+ annotation: null,
260
+ message: `cannot enumerate the scan corpus: ${error instanceof Error ? error.message : String(error)}`,
261
+ severity: 'fail',
262
+ });
263
+ return { ok: false, summary, findings, toolError: true };
264
+ }
265
+
266
+ for (const absolute of files) {
267
+ const relative = path.relative(pluginRoot, absolute);
268
+ /** @type {string} */
269
+ let body;
270
+ try {
271
+ body = readFileSync(absolute, 'utf8');
272
+ } catch (error) {
273
+ findings.push({
274
+ kind: 'tool-error',
275
+ file: relative,
276
+ line: 0,
277
+ path: '-',
278
+ annotation: null,
279
+ message: `cannot read: ${error instanceof Error ? error.message : String(error)}`,
280
+ severity: 'fail',
281
+ });
282
+ return { ok: false, summary, findings, toolError: true };
283
+ }
284
+ summary.filesScanned += 1;
285
+
286
+ const { citations, annotations, unbalancedFence } = extractCitations(body.split('\n'));
287
+ if (unbalancedFence) {
288
+ findings.push({
289
+ kind: 'unbalanced-fence',
290
+ file: relative,
291
+ line: unbalancedFence.line,
292
+ path: '-',
293
+ annotation: null,
294
+ message:
295
+ 'a code fence opens here and never closes — every line below it is invisible to this ' +
296
+ 'check (a fence closes only with the same character, at least as long, and no info ' +
297
+ 'string); close it or remove the stray marker',
298
+ severity: 'fail',
299
+ });
300
+ }
301
+ // Which lines carry a citation of their own. A marker that sits on such a
302
+ // line is that citation's OWN exemption and must not also reach downward.
303
+ const citedLines = new Set(citations.map((c) => c.line));
304
+
305
+ // A malformed marker is reported wherever it stands, even with nothing to
306
+ // exempt: it reads as an exemption and grants none.
307
+ for (const [line, annotation] of annotations) {
308
+ if (annotation.ok) continue;
309
+ findings.push({
310
+ kind: 'bad-annotation',
311
+ file: relative,
312
+ line,
313
+ path: '-',
314
+ annotation: annotation.raw,
315
+ message:
316
+ `malformed marker \`${annotation.raw}\` — expected \`path-check: planned #<iid>\`, ` +
317
+ '`path-check: historical` or `path-check: example`',
318
+ severity: 'fail',
319
+ });
320
+ }
321
+
322
+ for (const citation of citations) {
323
+ summary.citations += 1;
324
+ if (existsSync(path.join(pluginRoot, citation.path))) {
325
+ summary.existing += 1;
326
+ continue;
327
+ }
328
+ // An illustrative placeholder name needs no marker — see
329
+ // `isPlaceholderCitation`'s docblock for the closed fragment list and
330
+ // its named ceiling.
331
+ if (isPlaceholderCitation(citation.path)) {
332
+ summary.placeholders += 1;
333
+ continue;
334
+ }
335
+ // Same line, or the line immediately above — and the line above only
336
+ // when it carries NO citation itself. A `citation + marker` line is one
337
+ // self-contained exemption; letting it also cover the next line silently
338
+ // exempts a dead citation nobody ever annotated (the live shape at
339
+ // skills/wave-executor/wave-loop.md's `example` marker).
340
+ const above = citedLines.has(citation.line - 1)
341
+ ? undefined
342
+ : annotations.get(citation.line - 1);
343
+ const marker = annotations.get(citation.line) ?? above;
344
+ if (marker?.ok) {
345
+ summary.annotated += 1;
346
+ continue;
347
+ }
348
+ if (marker && !marker.ok) continue; // already reported as bad-annotation
349
+
350
+ // `.mjs` is blocking exactly as before this module grew a `.sh` half.
351
+ // `.sh` is advisory (`warn`) unless the caller opted into `strictSh`.
352
+ const isSh = path.extname(citation.path) === '.sh';
353
+ const severity = isSh && !strictSh ? 'warn' : 'fail';
354
+ if (severity === 'warn') summary.warnings += 1;
355
+ findings.push({
356
+ kind: 'missing-path',
357
+ file: relative,
358
+ line: citation.line,
359
+ path: citation.path,
360
+ annotation: null,
361
+ message:
362
+ (isSh
363
+ ? severity === 'warn'
364
+ ? `\`${citation.path}\` does not exist (advisory — .sh citations do not block ` +
365
+ 'validate-plugin until re-run with --strict-sh; see #1187) — '
366
+ : `\`${citation.path}\` does not exist (--strict-sh) — `
367
+ : `\`${citation.path}\` does not exist — `) +
368
+ 'create it, fix the path, or annotate the citation with ' +
369
+ '`<!-- path-check: planned #<iid> | historical | example -->` on this line or the ' +
370
+ 'line directly above',
371
+ severity,
372
+ });
373
+ }
374
+ }
375
+
376
+ findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
377
+ summary.findings = findings.length;
378
+ const blocking = findings.filter((f) => f.severity !== 'warn');
379
+ return { ok: blocking.length === 0, summary, findings, toolError: false };
380
+ }
381
+
382
+ /**
383
+ * Run the human-readable validator CLI.
384
+ *
385
+ * @param {string} pluginRoot absolute plugin root
386
+ * @returns {number} 0 = clean, 1 = findings, 2 = tool error
387
+ */
388
+ export function runCheckSkillScriptPaths(pluginRoot, { strictSh = false } = {}) {
389
+ console.log('--- Check: scripts/*.mjs (+ *.sh) paths cited in skills/commands/agents exist ---');
390
+ const inspection = scanSkillScriptPaths({ pluginRoot, strictSh });
391
+
392
+ for (const item of inspection.findings) {
393
+ // A `warn`-severity finding (a `.sh` citation, non-strict mode) is
394
+ // reported for visibility but must NOT print as ` FAIL:` — the
395
+ // validate-plugin aggregator counts failures by that exact 2-space
396
+ // prefix (`scripts/validate-plugin.mjs`'s `runCheck()`), so a `WARN:`
397
+ // line is how this check stays advisory end-to-end.
398
+ const label = item.severity === 'warn' ? 'WARN' : 'FAIL';
399
+ console.log(` ${label}: [${item.kind}] ${item.file}:${item.line} ${item.path} — ${item.message}`);
400
+ }
401
+ if (inspection.toolError) {
402
+ console.log('');
403
+ console.log(`Results: 0 passed, ${inspection.findings.length} failed`);
404
+ return 2;
405
+ }
406
+
407
+ const s = inspection.summary;
408
+ const blockingCount = inspection.findings.filter((f) => f.severity !== 'warn').length;
409
+ if (inspection.ok) {
410
+ console.log(
411
+ ` PASS: ${s.citations} script citation(s) in ${s.filesScanned} doc file(s) — ` +
412
+ `${s.existing} exist, ${s.annotated} annotated as deliberately absent, ` +
413
+ `${s.placeholders} placeholder(s)` +
414
+ (s.warnings > 0 ? `, ${s.warnings} advisory .sh warning(s) (see --strict-sh)` : ''),
415
+ );
416
+ }
417
+ console.log('');
418
+ console.log(`Results: ${inspection.ok ? 1 : 0} passed, ${blockingCount} failed`);
419
+ return inspection.ok ? 0 : 1;
420
+ }
421
+
422
+ const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
423
+ if (isMain) {
424
+ const strictSh = process.argv.includes('--strict-sh');
425
+ const args = process.argv.slice(2).filter((arg) => arg !== '--json' && arg !== '--strict-sh');
426
+ const root = path.resolve(args[0] || process.cwd());
427
+ if (process.argv.includes('--json')) {
428
+ const inspection = scanSkillScriptPaths({ pluginRoot: root, strictSh });
429
+ // Write, THEN set the exit code — `process.exit()` after a large print
430
+ // discards whatever is still queued on an async stdout pipe.
431
+ process.stdout.write(`${JSON.stringify(inspection, null, 2)}\n`);
432
+ process.exitCode = inspection.toolError ? 2 : inspection.ok ? 0 : 1;
433
+ } else {
434
+ process.exitCode = runCheckSkillScriptPaths(root, { strictSh });
435
+ }
436
+ }