session-orchestrator 5.1.0 → 5.2.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 (293) hide show
  1. package/.agents/skills/autopilot/SKILL.md +1 -0
  2. package/.agents/skills/bootstrap/SKILL.md +2 -0
  3. package/.agents/skills/brainstorm/SKILL.md +3 -0
  4. package/.agents/skills/close/SKILL.md +17 -0
  5. package/.agents/skills/debug/SKILL.md +2 -0
  6. package/.agents/skills/discovery/SKILL.md +2 -1
  7. package/.agents/skills/dispatcher/SKILL.md +2 -0
  8. package/.agents/skills/eli5/SKILL.md +2 -0
  9. package/.agents/skills/eval/SKILL.md +1 -0
  10. package/.agents/skills/evolve/SKILL.md +2 -1
  11. package/.agents/skills/go/SKILL.md +18 -0
  12. package/.agents/skills/grill/SKILL.md +2 -0
  13. package/.agents/skills/harness-audit/SKILL.md +16 -0
  14. package/.agents/skills/memory-cleanup/SKILL.md +1 -0
  15. package/.agents/skills/persona-panel/SKILL.md +1 -0
  16. package/.agents/skills/plan/SKILL.md +3 -1
  17. package/.agents/skills/portfolio/SKILL.md +17 -0
  18. package/.agents/skills/reconcile/SKILL.md +1 -0
  19. package/.agents/skills/release/SKILL.md +18 -0
  20. package/.agents/skills/repo-audit/SKILL.md +1 -0
  21. package/.agents/skills/spinout/SKILL.md +1 -0
  22. package/.agents/skills/sunset-review/SKILL.md +2 -0
  23. package/.agents/skills/test/SKILL.md +17 -0
  24. package/.agents/skills/ux-grill/SKILL.md +2 -0
  25. package/.claude-plugin/marketplace.json +1 -1
  26. package/.claude-plugin/plugin.json +1 -1
  27. package/.codex-plugin/plugin.json +1 -1
  28. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  29. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  30. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  31. package/.codex-plugin/skills/close/SKILL.md +3 -3
  32. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
  33. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
  34. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  35. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  36. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  37. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  38. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  39. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  40. package/.codex-plugin/skills/go/SKILL.md +3 -3
  41. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  42. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  43. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  44. package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
  45. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
  46. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  47. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  48. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  49. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  50. package/.codex-plugin/skills/release/SKILL.md +3 -3
  51. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  52. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  53. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  54. package/.codex-plugin/skills/test/SKILL.md +3 -3
  55. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  56. package/.cursor/commands/autopilot.md +4 -4
  57. package/.cursor/commands/bootstrap.md +5 -4
  58. package/.cursor/commands/brainstorm.md +5 -4
  59. package/.cursor/commands/close.md +4 -3
  60. package/.cursor/commands/convergence-monitoring.md +13 -0
  61. package/.cursor/commands/debug.md +4 -4
  62. package/.cursor/commands/discovery.md +4 -4
  63. package/.cursor/commands/dispatcher.md +4 -4
  64. package/.cursor/commands/eli5.md +4 -4
  65. package/.cursor/commands/eval.md +4 -4
  66. package/.cursor/commands/evolve.md +4 -4
  67. package/.cursor/commands/go.md +4 -3
  68. package/.cursor/commands/grill.md +4 -4
  69. package/.cursor/commands/harness-audit.md +3 -3
  70. package/.cursor/commands/memory-cleanup.md +4 -4
  71. package/.cursor/commands/npm-publish.md +13 -0
  72. package/.cursor/commands/persona-panel.md +4 -4
  73. package/.cursor/commands/plan.md +5 -4
  74. package/.cursor/commands/portfolio.md +3 -3
  75. package/.cursor/commands/reconcile.md +4 -4
  76. package/.cursor/commands/release.md +4 -3
  77. package/.cursor/commands/repo-audit.md +4 -4
  78. package/.cursor/commands/spinout.md +4 -4
  79. package/.cursor/commands/sunset-review.md +4 -4
  80. package/.cursor/commands/test.md +3 -3
  81. package/.cursor/commands/ux-grill.md +4 -4
  82. package/.cursor/rules/010-session-workflow.mdc +2 -2
  83. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  84. package/.cursor/skills/close/SKILL.md +13 -0
  85. package/.cursor/skills/debug/SKILL.md +0 -1
  86. package/.cursor/skills/discovery/SKILL.md +0 -1
  87. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  88. package/.cursor/skills/eli5/SKILL.md +0 -1
  89. package/.cursor/skills/evolve/SKILL.md +0 -1
  90. package/.cursor/skills/go/SKILL.md +13 -0
  91. package/.cursor/skills/grill/SKILL.md +0 -1
  92. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  93. package/.cursor/skills/portfolio/SKILL.md +12 -0
  94. package/.cursor/skills/release/SKILL.md +13 -0
  95. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  96. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  97. package/.cursor/skills/test/SKILL.md +12 -0
  98. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  99. package/.cursor-plugin/plugin.json +1 -1
  100. package/.orchestrator/policy/blocked-commands.json +1 -1
  101. package/AGENTS.md +1 -1
  102. package/CHANGELOG.md +61 -0
  103. package/README.md +11 -9
  104. package/commands/session.md +10 -0
  105. package/docs/ci-setup.md +53 -0
  106. package/docs/codex-setup.md +1 -1
  107. package/docs/components.md +11 -6
  108. package/docs/events-schema.md +4 -1
  109. package/docs/install.md +16 -0
  110. package/docs/persona-panel.md +1 -1
  111. package/docs/pi-setup.md +1 -1
  112. package/docs/rule-authoring.md +83 -14
  113. package/docs/scope-collision-guard.md +2 -0
  114. package/docs/session-config-reference.md +6 -4
  115. package/hooks/_lib/hook-import-set.json +46 -6
  116. package/hooks/_lib/subagent-paths.mjs +15 -0
  117. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/on-session-end.mjs +14 -2
  121. package/hooks/on-stop.mjs +43 -1
  122. package/hooks/post-bash-write-verify.mjs +3 -0
  123. package/hooks/pre-auq-clarity.mjs +3 -0
  124. package/hooks/pre-bash-issue-budget.mjs +103 -17
  125. package/hooks/pre-task-scope-disjoint.mjs +152 -3
  126. package/hooks/skill-invocation-telemetry.mjs +2 -1
  127. package/package.json +2 -1
  128. package/pi/prompts/autopilot.md +3 -3
  129. package/pi/prompts/bootstrap.md +3 -3
  130. package/pi/prompts/brainstorm.md +3 -3
  131. package/pi/prompts/close.md +2 -2
  132. package/pi/prompts/convergence-monitoring.md +11 -0
  133. package/pi/prompts/debug.md +3 -3
  134. package/pi/prompts/discovery.md +3 -3
  135. package/pi/prompts/dispatcher.md +3 -3
  136. package/pi/prompts/eli5.md +3 -3
  137. package/pi/prompts/eval.md +3 -3
  138. package/pi/prompts/evolve.md +3 -3
  139. package/pi/prompts/go.md +2 -2
  140. package/pi/prompts/grill.md +3 -3
  141. package/pi/prompts/harness-audit.md +2 -3
  142. package/pi/prompts/memory-cleanup.md +3 -3
  143. package/pi/prompts/npm-publish.md +11 -0
  144. package/pi/prompts/persona-panel.md +3 -3
  145. package/pi/prompts/plan.md +3 -3
  146. package/pi/prompts/portfolio.md +2 -2
  147. package/pi/prompts/reconcile.md +3 -3
  148. package/pi/prompts/release.md +3 -3
  149. package/pi/prompts/repo-audit.md +3 -4
  150. package/pi/prompts/session.md +1 -1
  151. package/pi/prompts/spinout.md +3 -3
  152. package/pi/prompts/sunset-review.md +3 -3
  153. package/pi/prompts/templates-ack.md +1 -1
  154. package/pi/prompts/test.md +3 -3
  155. package/pi/prompts/ux-grill.md +3 -3
  156. package/scripts/archive-closed-prds.mjs +2 -2
  157. package/scripts/auq-audit.mjs +2 -3
  158. package/scripts/backfill-abandoned-sessions.mjs +57 -3
  159. package/scripts/backfill-evidence-digest.mjs +2 -1
  160. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  161. package/scripts/check-package-manager.mjs +2 -2
  162. package/scripts/ci/assert-vitest-green.mjs +2 -1
  163. package/scripts/emit-session.mjs +2 -3
  164. package/scripts/export-hw-learnings.mjs +2 -1
  165. package/scripts/express-path.mjs +1 -1
  166. package/scripts/gc-stale-worktrees.mjs +2 -1
  167. package/scripts/generate-codex-skills.mjs +48 -4
  168. package/scripts/generate-cursor-adapter.mjs +173 -9
  169. package/scripts/generate-hook-import-set.mjs +12 -27
  170. package/scripts/generate-pi-prompts.mjs +183 -13
  171. package/scripts/github-protection-audit.mjs +2 -3
  172. package/scripts/lib/agent-frontmatter.mjs +23 -1
  173. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  174. package/scripts/lib/command-blocker.mjs +133 -5
  175. package/scripts/lib/config/drift-check.mjs +19 -0
  176. package/scripts/lib/convergence-monitor.mjs +2 -2
  177. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  178. package/scripts/lib/description-surface.mjs +2 -5
  179. package/scripts/lib/dispatcher/cli.mjs +2 -1
  180. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  181. package/scripts/lib/fetch-baseline.mjs +3 -8
  182. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  183. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  184. package/scripts/lib/instruction-budget-guard.mjs +186 -46
  185. package/scripts/lib/is-main-module.mjs +82 -0
  186. package/scripts/lib/locks/index.mjs +32 -25
  187. package/scripts/lib/maintenance-due-banner.mjs +69 -3
  188. package/scripts/lib/peer-discovery.mjs +2 -5
  189. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  190. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
  191. package/scripts/lib/rules-sync.mjs +2 -5
  192. package/scripts/lib/scope-echo.mjs +392 -7
  193. package/scripts/lib/session-close-backfill.mjs +58 -6
  194. package/scripts/lib/state-md.mjs +84 -3
  195. package/scripts/lib/sunset/walker.mjs +31 -4
  196. package/scripts/lib/tests-src-ratio.mjs +2 -6
  197. package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
  198. package/scripts/lib/user-invocable-skills.mjs +185 -0
  199. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  200. package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
  201. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  202. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  203. package/scripts/lib/validate/check-entry-guard.mjs +366 -0
  204. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  205. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  206. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  207. package/scripts/lib/validate/check-skill-links.mjs +27 -6
  208. package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
  209. package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
  210. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  211. package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
  212. package/scripts/lib/validate/check-unwired-features.mjs +91 -7
  213. package/scripts/lib/validate/check-validator-registration.mjs +2 -2
  214. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  215. package/scripts/lib/validate-vendored-rules.mjs +35 -9
  216. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  217. package/scripts/lock-reaper.mjs +2 -1
  218. package/scripts/materialize-wave-scope.mjs +87 -4
  219. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  220. package/scripts/migrate-vault-paths.mjs +2 -3
  221. package/scripts/release.mjs +80 -35
  222. package/scripts/relocate-vault-corpus.mjs +2 -3
  223. package/scripts/repair-invalid-sessions.mjs +2 -2
  224. package/scripts/session-shape.mjs +2 -2
  225. package/scripts/site-numbers.mjs +35 -11
  226. package/scripts/sweep-expired-rules.mjs +216 -0
  227. package/scripts/validate-plugin.mjs +9 -0
  228. package/scripts/vault-consolidate.mjs +2 -2
  229. package/scripts/vault-mirror.mjs +2 -3
  230. package/scripts/wave-scope-binding.mjs +2 -3
  231. package/skills/_shared/bootstrap-gate.md +1 -1
  232. package/skills/_shared/monitor-patterns.md +1 -1
  233. package/skills/_shared/research-evidence.md +53 -0
  234. package/skills/_shared/state-ownership.md +3 -0
  235. package/skills/autopilot/SKILL.md +58 -4
  236. package/skills/bootstrap/SKILL.md +51 -1
  237. package/skills/brainstorm/SKILL.md +16 -0
  238. package/skills/claude-md-drift-check/checker.mjs +49 -11
  239. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  240. package/skills/debug/SKILL.md +10 -0
  241. package/skills/discovery/SKILL.md +24 -1
  242. package/skills/discovery/probes-session.md +2 -2
  243. package/skills/dispatcher/SKILL.md +38 -7
  244. package/skills/eli5/SKILL.md +11 -0
  245. package/skills/eval/SKILL.md +14 -0
  246. package/skills/evolve/SKILL.md +8 -1
  247. package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
  248. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  249. package/skills/grill/SKILL.md +19 -0
  250. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  251. package/skills/hook-development/SKILL.md +46 -41
  252. package/skills/memory-cleanup/SKILL.md +7 -0
  253. package/skills/npm-publish/SKILL.md +1 -1
  254. package/skills/persona-panel/SKILL.md +56 -1
  255. package/skills/persona-panel/persona-format.md +1 -1
  256. package/skills/plan/SKILL.md +28 -1
  257. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  258. package/skills/reconcile/SKILL.md +10 -0
  259. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  260. package/skills/repo-audit/SKILL.md +7 -0
  261. package/skills/session-end/plan-verification.md +2 -2
  262. package/skills/session-plan/SKILL.md +1 -1
  263. package/skills/session-start/SKILL.md +5 -4
  264. package/skills/session-start/phase-8-5-express-path.md +6 -6
  265. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  266. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  267. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  268. package/skills/spinout/SKILL.md +12 -1
  269. package/skills/sunset-review/SKILL.md +13 -0
  270. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  271. package/skills/ux-grill/SKILL.md +19 -1
  272. package/skills/wave-executor/SKILL.md +7 -4
  273. package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
  274. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  275. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  276. package/commands/autopilot.md +0 -80
  277. package/commands/bootstrap.md +0 -56
  278. package/commands/brainstorm.md +0 -48
  279. package/commands/debug.md +0 -36
  280. package/commands/discovery.md +0 -32
  281. package/commands/dispatcher.md +0 -59
  282. package/commands/eli5.md +0 -33
  283. package/commands/eval.md +0 -28
  284. package/commands/evolve.md +0 -10
  285. package/commands/grill.md +0 -45
  286. package/commands/memory-cleanup.md +0 -26
  287. package/commands/persona-panel.md +0 -121
  288. package/commands/plan.md +0 -15
  289. package/commands/reconcile.md +0 -23
  290. package/commands/repo-audit.md +0 -24
  291. package/commands/spinout.md +0 -15
  292. package/commands/sunset-review.md +0 -27
  293. package/commands/ux-grill.md +0 -51
@@ -39,9 +39,10 @@
39
39
 
40
40
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
41
41
  import path from 'node:path';
42
- import { pathToFileURL } from 'node:url';
43
42
  import { normalizeSkillInvocation } from '../skill-invocations-schema.mjs';
44
43
  import { normalizeSkillJudgment } from '../skill-judgments-schema.mjs';
44
+ import { isMainModule } from '../is-main-module.mjs';
45
+ import { userInvocableSkills } from '../user-invocable-skills.mjs';
45
46
 
46
47
  // ---------------------------------------------------------------------------
47
48
  // Named threshold constants
@@ -602,6 +603,16 @@ export function staticReferenceScan(repoRoot, { kind, name }) {
602
603
  * Parse commands/*.md for the skill each command invokes. Recognises the
603
604
  * conventional `skills/<name>/SKILL.md` reference inside a command file.
604
605
  *
606
+ * NOTE since the 2026-09-16 command→skill fold (#1370): this linkage now covers
607
+ * almost nothing. `commands/` holds two files (`session`, `templates-ack`), so
608
+ * `skillToCommands` is near-empty by construction — the operator entry point for
609
+ * every other slash command is an explicit `user-invocable: true` in the skill's
610
+ * own frontmatter, not a command file pointing at it. `runSunsetWalk` therefore
611
+ * threads that flag alongside this linkage, and `classifyItem` treats the two as
612
+ * equivalent evidence of "an operator can invoke this directly". Without that, 26
613
+ * live slash commands would have been demoted the day their command file was
614
+ * deleted, for a reason that is a rename, not a disuse signal.
615
+ *
605
616
  * @param {string} repoRoot
606
617
  * @returns {{commandToSkill: Map<string,string|null>, skillToCommands: Map<string,string[]>}}
607
618
  */
@@ -772,6 +783,11 @@ export function classifyItem({
772
783
  const strictRefs = staticRefs.strictRefs;
773
784
  const nonBoilerplateRefs = staticRefs.nonBoilerplateRefs;
774
785
  const invokedByCommands = linkage?.invokedByCommands ?? [];
786
+ // #1370 fold: an explicit `user-invocable: true` in SKILL.md frontmatter is the
787
+ // post-fold form of "an operator invokes this directly" — the same evidence a
788
+ // command file used to carry. Default false so older callers/tests that do not
789
+ // thread it behave exactly as before.
790
+ const userInvocable = linkage?.userInvocable === true;
775
791
  const invokesSkill = linkage?.invokesSkill ?? null;
776
792
  // When a command invokes a skill, the caller passes whether that skill is
777
793
  // present on disk. Default-true so callers that don't thread the surface set
@@ -786,6 +802,7 @@ export function classifyItem({
786
802
  proseRefs,
787
803
  nonBoilerplateRefs,
788
804
  invokedByCommands,
805
+ userInvocable,
789
806
  invokesSkill,
790
807
  invokedSkillExists,
791
808
  lowConfidence,
@@ -842,6 +859,10 @@ export function classifyItem({
842
859
  active = true;
843
860
  reasons.push(`skill invoked by command(s): ${invokedByCommands.join(', ')}`);
844
861
  }
862
+ if (kind === 'skill' && userInvocable) {
863
+ active = true;
864
+ reasons.push('skill is an operator-facing slash command (user-invocable: true)');
865
+ }
845
866
  if (kind === 'command' && invokesSkill && invokedSkillExists) {
846
867
  active = true;
847
868
  reasons.push(`command invokes live skill: ${invokesSkill}`);
@@ -878,7 +899,10 @@ export function classifyItem({
878
899
 
879
900
  // --- Demote (near-zero) — skills & commands (agents handled above) ---------
880
901
  const lonelySkill =
881
- kind === 'skill' && nonBoilerplateRefs <= 1 && invokedByCommands.length === 0;
902
+ kind === 'skill' &&
903
+ nonBoilerplateRefs <= 1 &&
904
+ invokedByCommands.length === 0 &&
905
+ !userInvocable;
882
906
  const lonelyCommand =
883
907
  kind === 'command' && !invokesSkill && nonBoilerplateRefs <= 1;
884
908
  if (lonelySkill || lonelyCommand) {
@@ -942,6 +966,9 @@ export function runSunsetWalk(repoRoot, opts = {}) {
942
966
  // classification below behaves byte-for-byte identically to the L1-only path.
943
967
  const skillJudgments = readSkillJudgmentCounts(judgmentsPath, { windowDays, now: nowMs });
944
968
  const linkage = commandSkillLinkage(repoRoot);
969
+ // Post-#1370 replacement for the command→skill linkage as an operator-entry
970
+ // signal (see commandSkillLinkage's note). Read once per walk, not per skill.
971
+ const userInvocableSet = new Set(userInvocableSkills(repoRoot));
945
972
  const coverageDays = dispatch.coverageDays;
946
973
  const lowConfidence = coverageDays < windowDays;
947
974
 
@@ -967,7 +994,7 @@ export function runSunsetWalk(repoRoot, opts = {}) {
967
994
  name,
968
995
  dispatch: skillDispatch,
969
996
  static: staticRefs,
970
- linkage: { invokedByCommands },
997
+ linkage: { invokedByCommands, userInvocable: userInvocableSet.has(name) },
971
998
  windowDays,
972
999
  coverageDays,
973
1000
  judge,
@@ -1192,7 +1219,7 @@ function main(argv) {
1192
1219
  }
1193
1220
 
1194
1221
  // Import-safe main-guard.
1195
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
1222
+ if (isMainModule(import.meta.url)) {
1196
1223
  const code = main(process.argv.slice(2));
1197
1224
  process.exit(code);
1198
1225
  }
@@ -76,9 +76,9 @@
76
76
  import { readFileSync, existsSync, statSync } from 'node:fs';
77
77
  import { join, isAbsolute, relative, resolve } from 'node:path';
78
78
  import { execFileSync } from 'node:child_process';
79
- import { fileURLToPath } from 'node:url';
80
79
 
81
80
  import { writeStdoutLineSync } from './io.mjs';
81
+ import { isMainModule } from './is-main-module.mjs';
82
82
 
83
83
  // ---------------------------------------------------------------------------
84
84
  // The definition — the part that must never be re-derived by hand
@@ -475,10 +475,6 @@ function main() {
475
475
  process.exit(args.check && !result.withinCorridor ? 1 : 0);
476
476
  }
477
477
 
478
- const isMain =
479
- typeof process !== 'undefined' &&
480
- process.argv[1] !== null &&
481
- process.argv[1] !== undefined &&
482
- resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
478
+ const isMain =isMainModule(import.meta.url);
483
479
 
484
480
  if (isMain) main();
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { readFileSync, existsSync } from 'node:fs';
17
+ import { isMainModule } from '../is-main-module.mjs';
17
18
 
18
19
  const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
19
20
 
@@ -93,7 +94,7 @@ export function computeStats(events) {
93
94
  }
94
95
 
95
96
  // CLI entry-point — emit stats as JSON to stdout when run directly
96
- if (import.meta.url === `file://${process.argv[1]}`) {
97
+ if (isMainModule(import.meta.url)) {
97
98
  const events = readTmuxEvents();
98
99
  const stats = computeStats(events);
99
100
  console.log(JSON.stringify(stats, null, 2));
@@ -0,0 +1,185 @@
1
+ /**
2
+ * scripts/lib/user-invocable-skills.mjs
3
+ *
4
+ * ONE definition of "this skill is an operator-facing slash command".
5
+ *
6
+ * Since the #1370 command→skill fold (2026-09-16) `commands/` holds only the two
7
+ * names that cannot be a skill (`session`, `templates-ack` — see
8
+ * `tests/commands/headless-bare-command-availability.test.mjs` for the measured
9
+ * mechanism). Every other slash command IS a skill, marked by an EXPLICIT
10
+ * `user-invocable: true` in its SKILL.md frontmatter. Consumers that used to
11
+ * count or link `commands/*.md` therefore need the union:
12
+ *
13
+ * commands/*.md ∪ skills with explicit `user-invocable: true`
14
+ *
15
+ * This module exists so that union has one implementation rather than one per
16
+ * consumer (site-numbers tile, sunset walker linkage, the guard tests). It is
17
+ * deliberately tiny and dependency-free: `scripts/lib/sunset/walker.mjs` imports it,
18
+ * and the walker sits in the hook import graph, which must load WITHOUT
19
+ * node_modules (GH#62/#63) — so no `js-yaml` here; the frontmatter parser is the
20
+ * repo's own `parseAgentFrontmatter`, already in that graph.
21
+ *
22
+ * PARSED, never line-matched: `.claude/rules/test-hygiene.md` records that a
23
+ * line-regex frontmatter reader is blind to unparseable YAML and mis-measures
24
+ * block scalars. A SKILL.md whose frontmatter does not parse is treated as NOT
25
+ * user-invocable — fail-closed, and loud where it matters because such a file
26
+ * already fails `check-skills`/`validate-plugin`.
27
+ */
28
+
29
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
30
+ import path from 'node:path';
31
+ import { parseAgentFrontmatter } from './agent-frontmatter.mjs';
32
+
33
+ /**
34
+ * YAML 1.1 boolean lookalikes that YAML 1.2 / js-yaml's CORE_SCHEMA (what
35
+ * `generate-codex-skills` parses with) reads as PLAIN STRINGS, not as `true`.
36
+ * They are therefore NOT user-invocable here either — but silently demoting a
37
+ * skill on one of them is the #1370 defect class, so each one gets a WARN.
38
+ */
39
+ const TRUTHY_LOOKALIKE = /^(?:yes|y|on|t|1)$/i;
40
+
41
+ /**
42
+ * Strip a trailing `#` comment, honouring quotes: `true # note` is the same
43
+ * declaration as `true`, but `"a # b"` carries the hash as content.
44
+ *
45
+ * @param {string} text
46
+ * @returns {string}
47
+ */
48
+ function stripTrailingComment(text) {
49
+ let quote = null;
50
+ for (let i = 0; i < text.length; i += 1) {
51
+ const ch = text[i];
52
+ if (quote) {
53
+ if (ch === quote) quote = null;
54
+ continue;
55
+ }
56
+ if (ch === '"' || ch === "'") {
57
+ quote = ch;
58
+ continue;
59
+ }
60
+ // A `#` opens a YAML comment only at the start or after whitespace.
61
+ if (ch === '#' && (i === 0 || /\s/.test(text[i - 1]))) return text.slice(0, i);
62
+ }
63
+ return text;
64
+ }
65
+
66
+ /**
67
+ * Normalise a raw frontmatter scalar to the token a YAML parser would see:
68
+ * trim, drop a trailing comment, drop one layer of matching surrounding quotes.
69
+ *
70
+ * @param {string} text
71
+ * @returns {string}
72
+ */
73
+ function normaliseScalar(text) {
74
+ let out = stripTrailingComment(text).trim();
75
+ if (out.length >= 2 && (out[0] === '"' || out[0] === "'") && out[out.length - 1] === out[0]) {
76
+ out = out.slice(1, -1).trim();
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /**
82
+ * THE one normaliser for `user-invocable` — the marker "this skill is an
83
+ * operator-facing slash command". Every generator (Cursor, Pi, Codex) and every
84
+ * counter (site-numbers tile, drift-check `command-count`, the guard tests) must
85
+ * route through this function; four private re-implementations disagreeing on a
86
+ * quoted value is the defect this replaces.
87
+ *
88
+ * Explicit-true only. `undefined` (flag absent) is NOT user-invocable: the whole
89
+ * point of the marker is that it is written down, so a skill that forgot it is a
90
+ * defect to surface, not a default to guess.
91
+ *
92
+ * Accepted: the boolean `true`, and a string that normalises to `true`
93
+ * case-insensitively — so `"true"`, `'true'`, `True`, `true # note` and
94
+ * `true ` (trailing space, invisible in an editor) all read as the same
95
+ * declaration. Trailing-whitespace tolerance is load-bearing rather than
96
+ * cosmetic: without it a stray space silently demotes a skill out of the
97
+ * generated adapters with no diagnostic anywhere.
98
+ *
99
+ * REJECTED: `yes` / `y` / `on` / `t` / `1`. Those are YAML 1.1 booleans; under
100
+ * YAML 1.2 — which is what js-yaml's CORE_SCHEMA in `generate-codex-skills`
101
+ * applies — they are plain strings, and this predicate follows the parser rather
102
+ * than inventing a fifth dialect. Because the demotion is the surprising half,
103
+ * each one emits exactly one stderr WARN naming the file.
104
+ *
105
+ * @param {unknown} value the raw frontmatter value
106
+ * @param {string} [file] path named in the WARN when a truthy-looking value is demoted
107
+ * @returns {boolean}
108
+ */
109
+ export function isUserInvocableValue(value, file) {
110
+ if (value === true) return true;
111
+ if (typeof value !== 'string') return false;
112
+ const token = normaliseScalar(value);
113
+ if (token.toLowerCase() === 'true') return true;
114
+ if (TRUTHY_LOOKALIKE.test(token)) {
115
+ const where = file ? `${file}: ` : '';
116
+ process.stderr.write(
117
+ `WARN ${where}user-invocable: ${JSON.stringify(value)} is NOT a slash-command marker — `
118
+ + 'only `true` is (YAML 1.2 reads yes/on/1 as strings). Write `user-invocable: true`.\n',
119
+ );
120
+ }
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * Parse a SKILL.md's YAML frontmatter. Returns `null` when there is no
126
+ * frontmatter block or it does not parse.
127
+ *
128
+ * @param {string} content
129
+ * @returns {Record<string, unknown>|null}
130
+ */
131
+ export function parseSkillFrontmatter(content) {
132
+ if (typeof content !== 'string') return null;
133
+ const parsed = parseAgentFrontmatter(content);
134
+ return parsed.ok ? parsed.frontmatter : null;
135
+ }
136
+
137
+ /**
138
+ * Names of skills carrying an explicit `user-invocable: true`, sorted.
139
+ *
140
+ * @param {string} repoRoot plugin root (the directory holding `skills/`)
141
+ * @returns {string[]}
142
+ */
143
+ export function userInvocableSkills(repoRoot) {
144
+ const dir = path.join(repoRoot, 'skills');
145
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
146
+ const names = [];
147
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
148
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
149
+ const file = path.join(dir, entry.name, 'SKILL.md');
150
+ if (!existsSync(file) || !statSync(file).isFile()) continue;
151
+ let fm;
152
+ try {
153
+ fm = parseSkillFrontmatter(readFileSync(file, 'utf8'));
154
+ } catch {
155
+ continue;
156
+ }
157
+ if (fm && isUserInvocableValue(fm['user-invocable'], file)) names.push(entry.name);
158
+ }
159
+ return names.sort();
160
+ }
161
+
162
+ /** Names of `commands/*.md` files (without the extension), sorted. */
163
+ export function commandFileNames(repoRoot) {
164
+ const dir = path.join(repoRoot, 'commands');
165
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
166
+ return readdirSync(dir)
167
+ .filter((f) => f.endsWith('.md') && !f.startsWith('.'))
168
+ .map((f) => f.replace(/\.md$/, ''))
169
+ .sort();
170
+ }
171
+
172
+ /**
173
+ * The product's "slash commands" set: `commands/*.md` ∪ explicitly
174
+ * user-invocable skills, deduplicated by NAME.
175
+ *
176
+ * Dedup is load-bearing rather than defensive: a name present as BOTH is the
177
+ * picker-duplicate bug (the body runs, and the entry lists twice), so it must
178
+ * count once here and go red in the guard test — never inflate the number.
179
+ *
180
+ * @param {string} repoRoot
181
+ * @returns {string[]} sorted unique names
182
+ */
183
+ export function slashCommandNames(repoRoot) {
184
+ return [...new Set([...commandFileNames(repoRoot), ...userInvocableSkills(repoRoot)])].sort();
185
+ }
@@ -46,8 +46,8 @@
46
46
  import { execFileSync } from 'node:child_process';
47
47
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
48
48
  import path from 'node:path';
49
- import { pathToFileURL } from 'node:url';
50
49
  import { HISTORICAL_GUARD_BANNER } from '../historical-guard.mjs';
50
+ import { isMainModule } from '../is-main-module.mjs';
51
51
 
52
52
  /** Directories whose markdown quotes the banner as coordinator-facing prose. */
53
53
  const SCAN_DIRS = Object.freeze(['skills', 'commands']);
@@ -365,7 +365,7 @@ export function runCheckBannerParity(pluginRoot) {
365
365
  return inspection.toolError ? 2 : 1;
366
366
  }
367
367
 
368
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
368
+ const isMain =isMainModule(import.meta.url);
369
369
  if (isMain) {
370
370
  const pluginRoot = process.argv[2];
371
371
  if (!pluginRoot) {
@@ -6,11 +6,11 @@
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
8
8
  import { join, relative } from 'node:path';
9
- import { pathToFileURL } from 'node:url';
10
9
 
11
10
  import yaml from 'js-yaml';
12
11
 
13
12
  import { CURSOR_TO_CANONICAL_EVENT } from '../cursor-hook-bridge.mjs';
13
+ import { isMainModule } from '../is-main-module.mjs';
14
14
 
15
15
  /** agentskills.io caps a skill/command description at 1024 characters. */
16
16
  const DESCRIPTION_MAX = 1024;
@@ -259,6 +259,6 @@ function runCli(pluginRoot) {
259
259
  }
260
260
 
261
261
  // Direct invocation only — importing this module must not run the CLI.
262
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
262
+ if (isMainModule(import.meta.url)) {
263
263
  runCli(process.argv[2]);
264
264
  }
@@ -22,12 +22,12 @@
22
22
  // or process.exit — the isMain guard at the bottom is the only side-effecting
23
23
  // path. The Quality wave imports runCheckDeadBridge directly for unit tests.
24
24
 
25
- import { pathToFileURL } from 'node:url';
26
25
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
27
26
  import path from 'node:path';
28
27
  import { DETECTORS } from './dead-bridge-detectors.mjs';
29
28
  import * as corpus from './dead-bridge-corpus.mjs';
30
29
  import { listRepoFiles } from './repo-files.mjs';
30
+ import { isMainModule } from '../is-main-module.mjs';
31
31
 
32
32
  const DEFAULT_EXTS = ['.mjs', '.md'];
33
33
 
@@ -191,7 +191,7 @@ export function runCheckDeadBridge(pluginRoot) {
191
191
 
192
192
  // CLI entry — only when executed directly, never on import (keeps the exports
193
193
  // safe to import from tests without triggering process.exit).
194
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
194
+ const isMain =isMainModule(import.meta.url);
195
195
  if (isMain) {
196
196
  const pluginRoot = process.argv[2];
197
197
  if (!pluginRoot) {
@@ -65,9 +65,9 @@
65
65
  import { readFileSync } from 'node:fs';
66
66
  import path from 'node:path';
67
67
  import { spawnSync } from 'node:child_process';
68
- import { pathToFileURL } from 'node:url';
69
68
  import { listRepoFiles } from './repo-files.mjs';
70
69
  import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
70
+ import { isMainModule } from '../is-main-module.mjs';
71
71
 
72
72
  /** Directories walked for documentation. Mirrors check-vcs-repo-flag.mjs. */
73
73
  const SCAN_DIRS = Object.freeze([
@@ -461,7 +461,7 @@ export function runCheckDocCliCommands(pluginRoot) {
461
461
  return 0;
462
462
  }
463
463
 
464
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
464
+ const isMain =isMainModule(import.meta.url);
465
465
  if (isMain) {
466
466
  const args = process.argv.slice(2).filter((arg) => arg !== '--json');
467
467
  const root = path.resolve(args[0] || process.cwd());