session-orchestrator 5.0.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 (298) 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 +3 -3
  26. package/.claude-plugin/plugin.json +2 -2
  27. package/.codex-plugin/plugin.json +2 -2
  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 +2 -2
  100. package/.orchestrator/policy/blocked-commands.json +10 -0
  101. package/AGENTS.md +1 -1
  102. package/CHANGELOG.md +80 -0
  103. package/README.md +74 -235
  104. package/commands/session.md +10 -0
  105. package/docs/USER-GUIDE.md +24 -0
  106. package/docs/ci-setup.md +53 -0
  107. package/docs/codex-setup.md +1 -1
  108. package/docs/components.md +12 -5
  109. package/docs/events-schema.md +5 -1
  110. package/docs/install.md +128 -0
  111. package/docs/persona-panel.md +1 -1
  112. package/docs/pi-setup.md +1 -1
  113. package/docs/rule-authoring.md +83 -14
  114. package/docs/scope-collision-guard.md +2 -0
  115. package/docs/session-config-reference.md +6 -4
  116. package/docs/session-config-template.md +38 -0
  117. package/docs/telemetry.md +15 -0
  118. package/hooks/_lib/hook-import-set.json +46 -6
  119. package/hooks/_lib/subagent-paths.mjs +15 -0
  120. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  121. package/hooks/enforce-scope.mjs +42 -1
  122. package/hooks/hooks-codex.json +1 -1
  123. package/hooks/hooks.json +1 -1
  124. package/hooks/on-session-end.mjs +14 -2
  125. package/hooks/on-stop.mjs +43 -1
  126. package/hooks/post-bash-write-verify.mjs +3 -0
  127. package/hooks/pre-auq-clarity.mjs +3 -0
  128. package/hooks/pre-bash-issue-budget.mjs +103 -17
  129. package/hooks/pre-task-scope-disjoint.mjs +152 -3
  130. package/hooks/skill-invocation-telemetry.mjs +2 -1
  131. package/package.json +3 -2
  132. package/pi/prompts/autopilot.md +3 -3
  133. package/pi/prompts/bootstrap.md +3 -3
  134. package/pi/prompts/brainstorm.md +3 -3
  135. package/pi/prompts/close.md +2 -2
  136. package/pi/prompts/convergence-monitoring.md +11 -0
  137. package/pi/prompts/debug.md +3 -3
  138. package/pi/prompts/discovery.md +3 -3
  139. package/pi/prompts/dispatcher.md +3 -3
  140. package/pi/prompts/eli5.md +3 -3
  141. package/pi/prompts/eval.md +3 -3
  142. package/pi/prompts/evolve.md +3 -3
  143. package/pi/prompts/go.md +2 -2
  144. package/pi/prompts/grill.md +3 -3
  145. package/pi/prompts/harness-audit.md +2 -3
  146. package/pi/prompts/memory-cleanup.md +3 -3
  147. package/pi/prompts/npm-publish.md +11 -0
  148. package/pi/prompts/persona-panel.md +3 -3
  149. package/pi/prompts/plan.md +3 -3
  150. package/pi/prompts/portfolio.md +2 -2
  151. package/pi/prompts/reconcile.md +3 -3
  152. package/pi/prompts/release.md +3 -3
  153. package/pi/prompts/repo-audit.md +3 -4
  154. package/pi/prompts/session.md +1 -1
  155. package/pi/prompts/spinout.md +3 -3
  156. package/pi/prompts/sunset-review.md +3 -3
  157. package/pi/prompts/templates-ack.md +1 -1
  158. package/pi/prompts/test.md +3 -3
  159. package/pi/prompts/ux-grill.md +3 -3
  160. package/scripts/archive-closed-prds.mjs +2 -2
  161. package/scripts/auq-audit.mjs +2 -3
  162. package/scripts/backfill-abandoned-sessions.mjs +57 -3
  163. package/scripts/backfill-evidence-digest.mjs +2 -1
  164. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  165. package/scripts/check-package-manager.mjs +2 -2
  166. package/scripts/ci/assert-vitest-green.mjs +2 -1
  167. package/scripts/emit-session.mjs +2 -3
  168. package/scripts/export-hw-learnings.mjs +2 -1
  169. package/scripts/express-path.mjs +1 -1
  170. package/scripts/gc-stale-worktrees.mjs +2 -1
  171. package/scripts/generate-codex-skills.mjs +48 -4
  172. package/scripts/generate-cursor-adapter.mjs +173 -9
  173. package/scripts/generate-hook-import-set.mjs +12 -27
  174. package/scripts/generate-pi-prompts.mjs +183 -13
  175. package/scripts/github-protection-audit.mjs +2 -3
  176. package/scripts/lib/agent-frontmatter.mjs +23 -1
  177. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  178. package/scripts/lib/command-blocker.mjs +209 -9
  179. package/scripts/lib/config/drift-check.mjs +19 -0
  180. package/scripts/lib/convergence-monitor.mjs +2 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  182. package/scripts/lib/description-surface.mjs +2 -5
  183. package/scripts/lib/dispatcher/cli.mjs +2 -1
  184. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  185. package/scripts/lib/fetch-baseline.mjs +3 -8
  186. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  187. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  188. package/scripts/lib/instruction-budget-guard.mjs +186 -46
  189. package/scripts/lib/is-main-module.mjs +82 -0
  190. package/scripts/lib/locks/index.mjs +32 -25
  191. package/scripts/lib/maintenance-due-banner.mjs +69 -3
  192. package/scripts/lib/peer-discovery.mjs +2 -5
  193. package/scripts/lib/playwright-driver/runner.mjs +63 -2
  194. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
  195. package/scripts/lib/rules-sync.mjs +2 -5
  196. package/scripts/lib/scope-echo.mjs +392 -7
  197. package/scripts/lib/session-close-backfill.mjs +58 -6
  198. package/scripts/lib/state-md.mjs +84 -3
  199. package/scripts/lib/sunset/walker.mjs +31 -4
  200. package/scripts/lib/tests-src-ratio.mjs +2 -6
  201. package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
  202. package/scripts/lib/user-invocable-skills.mjs +185 -0
  203. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  204. package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
  205. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  206. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  207. package/scripts/lib/validate/check-entry-guard.mjs +366 -0
  208. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  209. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  210. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  211. package/scripts/lib/validate/check-skill-links.mjs +27 -6
  212. package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
  213. package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
  214. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  215. package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
  216. package/scripts/lib/validate/check-unwired-features.mjs +266 -11
  217. package/scripts/lib/validate/check-validator-registration.mjs +2 -2
  218. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  219. package/scripts/lib/validate-vendored-rules.mjs +35 -9
  220. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  221. package/scripts/lock-reaper.mjs +2 -1
  222. package/scripts/materialize-wave-scope.mjs +87 -4
  223. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  224. package/scripts/migrate-vault-paths.mjs +2 -3
  225. package/scripts/release.mjs +124 -35
  226. package/scripts/relocate-vault-corpus.mjs +2 -3
  227. package/scripts/repair-invalid-sessions.mjs +2 -2
  228. package/scripts/session-shape.mjs +2 -2
  229. package/scripts/site-numbers.mjs +35 -11
  230. package/scripts/sweep-expired-rules.mjs +216 -0
  231. package/scripts/validate-plugin.mjs +9 -0
  232. package/scripts/vault-consolidate.mjs +2 -2
  233. package/scripts/vault-mirror.mjs +2 -3
  234. package/scripts/wave-scope-binding.mjs +2 -3
  235. package/skills/_shared/bootstrap-gate.md +1 -1
  236. package/skills/_shared/monitor-patterns.md +1 -1
  237. package/skills/_shared/research-evidence.md +53 -0
  238. package/skills/_shared/state-ownership.md +3 -0
  239. package/skills/autopilot/SKILL.md +58 -4
  240. package/skills/bootstrap/SKILL.md +51 -1
  241. package/skills/brainstorm/SKILL.md +16 -0
  242. package/skills/claude-md-drift-check/checker.mjs +49 -11
  243. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  244. package/skills/debug/SKILL.md +10 -0
  245. package/skills/discovery/SKILL.md +24 -1
  246. package/skills/discovery/probes-session.md +2 -2
  247. package/skills/dispatcher/SKILL.md +38 -7
  248. package/skills/eli5/SKILL.md +11 -0
  249. package/skills/eval/SKILL.md +14 -0
  250. package/skills/evolve/SKILL.md +8 -1
  251. package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
  252. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  253. package/skills/grill/SKILL.md +19 -0
  254. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  255. package/skills/hook-development/SKILL.md +46 -41
  256. package/skills/memory-cleanup/SKILL.md +7 -0
  257. package/skills/npm-publish/SKILL.md +1 -1
  258. package/skills/persona-panel/SKILL.md +56 -1
  259. package/skills/persona-panel/persona-format.md +1 -1
  260. package/skills/plan/SKILL.md +28 -1
  261. package/skills/playwright-driver/SKILL.md +7 -10
  262. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  263. package/skills/reconcile/SKILL.md +10 -0
  264. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  265. package/skills/repo-audit/SKILL.md +7 -0
  266. package/skills/session-end/plan-verification.md +2 -2
  267. package/skills/session-plan/SKILL.md +1 -1
  268. package/skills/session-start/SKILL.md +5 -4
  269. package/skills/session-start/phase-8-5-express-path.md +6 -6
  270. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  271. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  272. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  273. package/skills/spinout/SKILL.md +12 -1
  274. package/skills/sunset-review/SKILL.md +13 -0
  275. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  276. package/skills/ux-grill/SKILL.md +19 -1
  277. package/skills/wave-executor/SKILL.md +7 -4
  278. package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
  279. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  280. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  281. package/commands/autopilot.md +0 -80
  282. package/commands/bootstrap.md +0 -56
  283. package/commands/brainstorm.md +0 -48
  284. package/commands/debug.md +0 -36
  285. package/commands/discovery.md +0 -32
  286. package/commands/dispatcher.md +0 -59
  287. package/commands/eli5.md +0 -33
  288. package/commands/eval.md +0 -28
  289. package/commands/evolve.md +0 -10
  290. package/commands/grill.md +0 -45
  291. package/commands/memory-cleanup.md +0 -26
  292. package/commands/persona-panel.md +0 -121
  293. package/commands/plan.md +0 -15
  294. package/commands/reconcile.md +0 -23
  295. package/commands/repo-audit.md +0 -24
  296. package/commands/spinout.md +0 -15
  297. package/commands/sunset-review.md +0 -27
  298. package/commands/ux-grill.md +0 -51
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-entry-guard.mjs — census of SYMLINK-FRAGILE ESM entry guards.
4
+ *
5
+ * ## The defect class (#1371)
6
+ *
7
+ * A module that runs `main()` only when invoked as a CLI compares "the module I
8
+ * am" against "the script Node was told to run". The two are not the same kind
9
+ * of path: Node resolves `import.meta.url` to the **realpath**, while
10
+ * `process.argv[1]` is the path **as typed**. With a symlink anywhere in the
11
+ * invocation path — a `node_modules/.bin/` shim, a `~/bin` convenience link, a
12
+ * plugin directory symlinked into `~/.claude/plugins/`, or `/tmp` itself (a
13
+ * symlink to `/private/tmp` on macOS) — the comparison is false.
14
+ *
15
+ * The failure mode is the worst shape a defect can have: `main()` never runs,
16
+ * nothing is printed, and the process exits 0. Every caller reads that as
17
+ * success.
18
+ *
19
+ * The fix is `isMainModule()` from `scripts/lib/is-main-module.mjs`, which
20
+ * realpaths BOTH sides. This check is the recurrence guard: it fails the build
21
+ * when a new hand-written comparison appears.
22
+ *
23
+ * ## Oracle
24
+ *
25
+ * Over every tracked `.mjs` under `scripts/` + `hooks/` (via `git ls-files`,
26
+ * so an untracked scratch file is never a finding):
27
+ *
28
+ * 1. strip comments (line comments, trailing `//`, and block comments),
29
+ * 2. split the remainder into statement fragments on `;{}`,
30
+ * 3. inside each fragment mentioning `process.argv[1]`, apply two oracles:
31
+ * a. **comparison form** — take every `===` / `!==` comparison and its two
32
+ * operands (bounded to the adjacent `&&` / `||` segment) and FAIL when
33
+ * one operand mentions `process.argv[1]` and the other mentions
34
+ * `import.meta.url` or `__filename`;
35
+ * b. **bare-basename form** — FAIL on `process.argv[1].endsWith('x.mjs')`
36
+ * (plus the `?.`, `String(...)` and `(argv[1] || '')` receiver
37
+ * variants) against a `.mjs`/`.js`/`.cjs` string literal. This form has
38
+ * no comparison operator at all, so oracle (a) never saw it.
39
+ * 4. In both cases, a fragment wrapping either side in a `realpath*` call is
40
+ * the safe idiom and must not be flagged.
41
+ *
42
+ * Both oracles are anchored on a SYNTAX shape, which is what keeps prose out of
43
+ * the census: `scripts/validate-plugin.mjs`,
44
+ * `scripts/lib/vault-consolidate-fs.mjs`, `hooks/post-edit-import-probe.mjs`
45
+ * and this file's own header all MENTION a broken idiom inside a comment and
46
+ * are excluded by step 1 without needing an allowlist entry.
47
+ * `scripts/lib/baseline-archetypes.mjs` (`await import(process.argv[1])`) is a
48
+ * deliberate cross-process trick that is neither a comparison nor a basename
49
+ * test — out of scope for this check.
50
+ *
51
+ * `scripts/lib/ecosystem-wizard.mjs` (bare `argv[1].endsWith(...)`) and
52
+ * `scripts/lib/fetch-baseline.mjs` (a hybrid `===`-plus-`endsWith` guard, which
53
+ * needed the ALLOWLIST below) were the two hand-written guards this check was
54
+ * built to catch (#1371); both were fixed to `isMainModule()` in #1378. Oracle
55
+ * (b) was added afterwards because reverting the FIRST of them was caught by
56
+ * nothing: the bare form was outside oracle (a) by construction, and neither
57
+ * file's symlink smoke test reproduced its own regression (a same-named file
58
+ * behind a symlinked DIRECTORY still ends with the basename). The allowlist is
59
+ * empty — kept as a frozen mechanism (see its docblock) rather than deleted,
60
+ * since the next hand-written exception is exactly what would re-populate it.
61
+ *
62
+ * `scripts/lib/is-main-module.mjs` itself is excluded: it is the fix, and its
63
+ * docblock quotes every variant it replaces.
64
+ *
65
+ * ## Mode: BLOCKING
66
+ *
67
+ * Unlike the WARN-only censuses in validate-plugin (`check-unwired-features`,
68
+ * `check-learning-provenance`, `check-vcs-repo-flag`), this one blocks. It can
69
+ * do so honestly because the backlog was drained to zero in the same change
70
+ * that introduced it — a blocking gate is only dishonest when it is red on
71
+ * arrival for work nobody intends to do.
72
+ */
73
+
74
+ import { execFileSync } from 'node:child_process';
75
+ import { readFileSync } from 'node:fs';
76
+ import path from 'node:path';
77
+
78
+ import { isMainModule } from '../is-main-module.mjs';
79
+
80
+ /**
81
+ * Files whose `process.argv[1]` comparison the oracle flags but which are NOT
82
+ * the defect class. Every entry carries the reason it is exempt; a stale entry
83
+ * (one the oracle no longer flags) is itself reported, so this list cannot rot
84
+ * into a silent suppression.
85
+ *
86
+ * Empty since #1378: the one entry this list ever carried
87
+ * (`scripts/lib/fetch-baseline.mjs`, whose hybrid guard's `endsWith(basename)`
88
+ * fallback made it non-fragile) was removed when that file was fixed to
89
+ * `isMainModule()` instead of being kept as a hand-verified exception. The
90
+ * mechanism is retained rather than deleted: the stale-entry check below only
91
+ * fires for an entry that is PRESENT and no longer flagged, so an empty object
92
+ * never trips it, and a future hand-written exception can still be recorded
93
+ * here.
94
+ *
95
+ * @type {Record<string, string>}
96
+ */
97
+ const ALLOWLIST = {};
98
+
99
+ /** The one module allowed to contain every broken variant: it documents them. */
100
+ const SELF_EXEMPT = 'scripts/lib/is-main-module.mjs';
101
+
102
+ /**
103
+ * Remove comments while preserving character offsets (so line numbers survive).
104
+ *
105
+ * @param {string} src module source
106
+ * @returns {string} same length, comment bytes replaced by spaces
107
+ */
108
+ export function stripComments(src) {
109
+ const out = src.split('');
110
+ let i = 0;
111
+ let mode = 'code';
112
+ while (i < src.length) {
113
+ const two = src.slice(i, i + 2);
114
+ if (mode === 'code') {
115
+ if (two === '//') mode = 'line';
116
+ else if (two === '/*') mode = 'block';
117
+ else if (src[i] === '`' || src[i] === "'" || src[i] === '"') {
118
+ // Skip OVER the literal to its closing delimiter, leaving the contents
119
+ // intact. This is load-bearing, not hygiene: the V1 idiom is
120
+ // \`file://${process.argv[1]}\` and the `//` in `file://` reads as a line
121
+ // comment to a naive scanner, which blanks the rest of the line and
122
+ // makes the most common variant of the defect class invisible.
123
+ const quote = src[i];
124
+ i++;
125
+ while (i < src.length && src[i] !== quote) {
126
+ if (src[i] === '\\') i++;
127
+ i++;
128
+ }
129
+ i++;
130
+ continue;
131
+ }
132
+ }
133
+ if (mode === 'line') {
134
+ if (src[i] === '\n') mode = 'code';
135
+ else out[i] = ' ';
136
+ } else if (mode === 'block') {
137
+ if (src[i] !== '\n') out[i] = ' ';
138
+ if (two === '*/') {
139
+ out[i] = ' ';
140
+ out[i + 1] = ' ';
141
+ i += 2;
142
+ mode = 'code';
143
+ continue;
144
+ }
145
+ }
146
+ i++;
147
+ }
148
+ return out.join('');
149
+ }
150
+
151
+ /**
152
+ * A `.endsWith('<something>.mjs')` call with a module-basename-shaped literal.
153
+ * Scanned rather than matched in one shot so the RECEIVER can be judged from the
154
+ * text preceding each hit — a `.endsWith()` on anything other than
155
+ * `process.argv[1]` is not this defect class.
156
+ *
157
+ * The quote characters are spelled `\x27` / `\x22` / `\x60` rather than
158
+ * literally, and that is load-bearing for THIS file specifically: `stripComments`
159
+ * below has no notion of a regex literal, so a bare `'` inside one reads as the
160
+ * start of a string and desynchronises the scanner for the rest of the module —
161
+ * measured 2026-09-17, a literal-quote version of this regex made the census
162
+ * report a line-comment 40 lines further down as a finding. Same rule applies to
163
+ * every regex added to this module.
164
+ */
165
+ const ENDS_WITH_LITERAL = /endsWith\(\s*([\x27\x22\x60])([^\x27\x22\x60]*)\1\s*\)/g;
166
+
167
+ /**
168
+ * The receiver chain, whitespace-stripped, that makes an `.endsWith()` call a
169
+ * test on the invocation path. Deliberately unanchored at the START so an outer
170
+ * wrapper (`String(...)`, `path.basename(...)`) still matches; anchored at the
171
+ * END so only the call immediately downstream of `process.argv[1]` counts.
172
+ */
173
+ const ARGV_RECEIVER_TAIL = /process\.argv\[1\](?:\|\|\x27\x27|\|\|\x22\x22)?\)*\??\.$/;
174
+
175
+ /**
176
+ * Findings for one module body.
177
+ *
178
+ * `kind` distinguishes the two oracles: `comparison` is the
179
+ * `argv[1] === import.meta.url` family, `basename` the bare
180
+ * `argv[1].endsWith('x.mjs')` family. Both are symlink-fragile; only the remedy
181
+ * wording differs.
182
+ *
183
+ * @param {string} src module source
184
+ * @returns {Array<{line: number, text: string, kind: 'comparison'|'basename'}>} one per fragile guard
185
+ */
186
+ export function findFragileGuards(src) {
187
+ // Template substitutions are rewritten `${x}` → `$(x)` BEFORE the fragment
188
+ // split: the V1 idiom `import.meta.url === \`file://${process.argv[1]}\`` puts
189
+ // braces INSIDE the comparison, so splitting on `{}` would tear the statement
190
+ // in two and the most common variant of the defect class (12 of 50 files at
191
+ // ca214376) would never be seen. Length is preserved so line numbers survive.
192
+ const stripped = stripComments(src).replace(/\$\{([^{}]*)\}/g, (_m, inner) => `$(${inner})`);
193
+ const findings = [];
194
+ let cursor = 0;
195
+ for (const fragment of stripped.split(/[;{}]/)) {
196
+ const fragStart = cursor;
197
+ cursor += fragment.length + 1;
198
+ if (!fragment.includes('process.argv[1]')) continue;
199
+
200
+ /** @type {(offset: number) => number} 1-based line of an offset in `fragment` */
201
+ const lineAt = (offset) =>
202
+ stripped.slice(0, Math.min(fragStart + offset, stripped.length)).split('\n').length;
203
+
204
+ if (!/import\.meta\.url|__filename/.test(fragment)) {
205
+ // Oracle (b): no self-path mention at all, so the comparison oracle below
206
+ // cannot see this fragment. The bare basename test lives exactly here.
207
+ if (!/realpath/i.test(fragment)) {
208
+ ENDS_WITH_LITERAL.lastIndex = 0;
209
+ let m;
210
+ while ((m = ENDS_WITH_LITERAL.exec(fragment)) !== null) {
211
+ if (!/\.(?:mjs|js|cjs)$/.test(m[2])) continue;
212
+ const receiver = fragment.slice(0, m.index).replace(/\s+/g, '');
213
+ if (!ARGV_RECEIVER_TAIL.test(receiver)) continue;
214
+ findings.push({
215
+ line: lineAt(m.index),
216
+ text: `process.argv[1]…${m[0]}`,
217
+ kind: 'basename',
218
+ });
219
+ break; // one finding per statement is enough to act on
220
+ }
221
+ }
222
+ continue;
223
+ }
224
+
225
+ const parts = fragment.split(/===|!==/);
226
+ for (let k = 0; k < parts.length - 1; k++) {
227
+ const left = parts[k].split(/&&|\|\|/).pop() ?? '';
228
+ const right = parts[k + 1].split(/&&|\|\|/)[0] ?? '';
229
+ const mentionsArgv = /process\.argv\[1\]/.test(left) || /process\.argv\[1\]/.test(right);
230
+ const mentionsSelf =
231
+ /import\.meta\.url|__filename/.test(left) || /import\.meta\.url|__filename/.test(right);
232
+ if (!mentionsArgv || !mentionsSelf) continue;
233
+ if (/realpath/i.test(left) || /realpath/i.test(right)) continue; // the safe idiom
234
+ const line = lineAt(parts.slice(0, k + 1).join('===').length || 0);
235
+ findings.push({
236
+ line,
237
+ text: `${left.trim()} === ${right.trim()}`.replace(/\s+/g, ' '),
238
+ kind: 'comparison',
239
+ });
240
+ break; // one finding per statement is enough to act on
241
+ }
242
+ }
243
+ return findings;
244
+ }
245
+
246
+ /**
247
+ * Tracked `.mjs` files under `scripts/` and `hooks/`.
248
+ *
249
+ * @param {string} repoRoot absolute repo root
250
+ * @returns {string[]} repo-relative POSIX paths
251
+ */
252
+ function trackedModules(repoRoot) {
253
+ // Pathspecs are DIRECTORIES, not `scripts/**/*.mjs`: git's wildmatch requires
254
+ // `**` to consume at least one path component, so a `scripts/**/*.mjs` spec
255
+ // silently drops every top-level `scripts/*.mjs` AND all of `hooks/*.mjs` —
256
+ // measured 2026-09-16: 61 files matched vs 176 actually tracked. The extension
257
+ // filter therefore lives here, where it cannot be wrong about that.
258
+ //
259
+ // Re-measured 2026-09-17 (the 61/176 pair no longer reproduces at any scope I
260
+ // could reconstruct, so it is kept as-dated rather than edited):
261
+ // `git ls-files -- 'scripts/**/*.mjs' 'hooks/**/*.mjs' | wc -l` → 422 against
262
+ // `git ls-files -- scripts hooks | grep -c '\.mjs$'` → 508. Direction of the
263
+ // trap is unchanged; only the magnitude moved.
264
+ const out = execFileSync('git', ['ls-files', '--', 'scripts', 'hooks'], {
265
+ cwd: repoRoot,
266
+ encoding: 'utf8',
267
+ maxBuffer: 32 * 1024 * 1024,
268
+ });
269
+ return out
270
+ .split('\n')
271
+ .map((l) => l.trim())
272
+ .filter((l) => l.endsWith('.mjs'));
273
+ }
274
+
275
+ /**
276
+ * Run the census.
277
+ *
278
+ * @param {string} repoRoot absolute repo root
279
+ * @returns {Promise<number>} 0 clean · 1 findings · 2 tool error
280
+ */
281
+ export async function runCheckEntryGuard(repoRoot) {
282
+ let files;
283
+ try {
284
+ files = trackedModules(repoRoot);
285
+ } catch (err) {
286
+ console.log(` FAIL: cannot enumerate tracked modules: ${err.message}`);
287
+ console.log('');
288
+ console.log('Results: 0 passed, 1 failed');
289
+ return 2;
290
+ }
291
+
292
+ let failed = 0;
293
+ const flagged = new Set();
294
+ for (const rel of files) {
295
+ if (rel === SELF_EXEMPT) continue;
296
+ let body;
297
+ try {
298
+ body = readFileSync(path.join(repoRoot, rel), 'utf8');
299
+ } catch {
300
+ continue; // deleted between ls-files and read — not this check's business
301
+ }
302
+ const findings = findFragileGuards(body);
303
+ if (findings.length === 0) continue;
304
+ flagged.add(rel);
305
+ if (rel in ALLOWLIST) continue;
306
+ for (const f of findings) {
307
+ const why =
308
+ f.kind === 'basename'
309
+ ? 'a basename test answers "was a file with this name run?", never "was THIS ' +
310
+ 'module run" — it is true for any same-named file (a sibling copy, a ' +
311
+ 'vendored duplicate, a fixture) and resolves no path at all, so main() ' +
312
+ 'fires or stays silent for reasons unrelated to identity'
313
+ : '`import.meta.url` is the realpath, `process.argv[1]` is the path as typed, ' +
314
+ 'so the guard is false under any symlinked invocation and main() silently ' +
315
+ 'never runs (exit 0, no output)';
316
+ console.log(
317
+ ` FAIL: ${rel}:${f.line} — symlink-fragile entry guard \`${f.text}\`: ${why}. ` +
318
+ 'Use `isMainModule(import.meta.url)` from scripts/lib/is-main-module.mjs',
319
+ );
320
+ failed++;
321
+ }
322
+ }
323
+
324
+ const censused = new Set(files);
325
+ for (const [rel, reason] of Object.entries(ALLOWLIST)) {
326
+ if (flagged.has(rel)) continue;
327
+ // Not present at all (a fixture repo, a fork that deleted the file) is not
328
+ // rot — only an entry whose file IS censused and no longer flagged is.
329
+ if (!censused.has(rel)) continue;
330
+ console.log(
331
+ ` FAIL: ${rel} — stale allowlist entry: the oracle no longer flags this file, ` +
332
+ `so the exemption (“${reason}”) suppresses nothing. Remove it.`,
333
+ );
334
+ failed++;
335
+ }
336
+
337
+ if (failed === 0) {
338
+ console.log(
339
+ ` PASS: censused ${files.length} tracked module(s) under scripts/ + hooks/ — ` +
340
+ `0 symlink-fragile entry guard(s), ${Object.keys(ALLOWLIST).length} allowlisted`,
341
+ );
342
+ }
343
+ console.log('');
344
+ console.log(`Results: ${failed === 0 ? 1 : 0} passed, ${failed} failed`);
345
+ return failed === 0 ? 0 : 1;
346
+ }
347
+
348
+ if (isMainModule(import.meta.url)) {
349
+ const argv = process.argv.slice(2);
350
+ const usage =
351
+ 'Usage: check-entry-guard.mjs [<repo-root>]\n' +
352
+ 'Exit: 0 clean · 1 symlink-fragile entry guard(s) found · 2 tool error';
353
+ if (argv.includes('--help')) {
354
+ console.log(usage);
355
+ process.exitCode = 0;
356
+ } else {
357
+ const positional = argv.filter((a) => !a.startsWith('--'));
358
+ const unknown = argv.filter((a) => a.startsWith('--') && a !== '--help');
359
+ if (unknown.length > 0) {
360
+ console.error(`Unknown flag(s): ${unknown.join(', ')}\n${usage}`);
361
+ process.exitCode = 1;
362
+ } else {
363
+ process.exitCode = await runCheckEntryGuard(path.resolve(positional[0] ?? process.cwd()));
364
+ }
365
+ }
366
+ }
@@ -14,8 +14,8 @@
14
14
  import { spawnSync } from 'node:child_process';
15
15
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
16
16
  import path from 'node:path';
17
- import { pathToFileURL } from 'node:url';
18
17
  import { parse } from '@babel/parser';
18
+ import { isMainModule } from '../is-main-module.mjs';
19
19
 
20
20
  const HOOKS_RELATIVE_DIR = 'hooks';
21
21
  const MODULE_RELATIVE_PREFIX = 'scripts/lib';
@@ -1137,7 +1137,7 @@ export function runCheckGuardRequiresParity(pluginRoot) {
1137
1137
  return inspection.toolError ? 2 : 1;
1138
1138
  }
1139
1139
 
1140
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
1140
+ const isMain =isMainModule(import.meta.url);
1141
1141
  if (isMain) {
1142
1142
  const pluginRoot = process.argv[2];
1143
1143
  if (!pluginRoot) {
@@ -79,9 +79,9 @@
79
79
 
80
80
  import { readFileSync } from 'node:fs';
81
81
  import path from 'node:path';
82
- import { pathToFileURL } from 'node:url';
83
82
  import { parse } from '@babel/parser';
84
83
  import { listRepoFiles } from './repo-files.mjs';
84
+ import { isMainModule } from '../is-main-module.mjs';
85
85
 
86
86
  // ---------------------------------------------------------------------------
87
87
  // Baseline — Map<key, reason>, keyed on
@@ -359,7 +359,7 @@ export function runCheckHooksEmitEventGuard(repoRoot, options) {
359
359
  return failCount > 0 ? 1 : 0;
360
360
  }
361
361
 
362
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
362
+ const isMain =isMainModule(import.meta.url);
363
363
  if (isMain) {
364
364
  const root = process.argv[2];
365
365
  if (!root) {
@@ -126,7 +126,6 @@
126
126
 
127
127
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
128
128
  import path from 'node:path';
129
- import { pathToFileURL } from 'node:url';
130
129
  import { readLearnings } from '../learnings/io.mjs';
131
130
  // THE shared key derivation. This check resolves a rule's STAMPED
132
131
  // `learning-key` against keys it derives from the store, so it is the one place
@@ -134,6 +133,7 @@ import { readLearnings } from '../learnings/io.mjs';
134
133
  // finding — it must derive the key with the emitter's function, not a copy of
135
134
  // the emitter's formula.
136
135
  import { learningKeyOf } from '../learnings/kebab.mjs';
136
+ import { isMainModule } from '../is-main-module.mjs';
137
137
 
138
138
  /** Directory holding the rule corpus, relative to the plugin root. */
139
139
  const RULES_REL = path.join('.claude', 'rules');
@@ -475,7 +475,7 @@ export async function runCheckLearningProvenance(pluginRoot) {
475
475
  return 0;
476
476
  }
477
477
 
478
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
478
+ const isMain =isMainModule(import.meta.url);
479
479
  if (isMain) {
480
480
  const argv = process.argv.slice(2);
481
481
  const flags = new Set(argv.filter((a) => a.startsWith('--')));
@@ -20,7 +20,7 @@
20
20
  * citations, not intra-surface markdown links. Different predicate, different blind spot.
21
21
  *
22
22
  * WHAT IT CHECKS
23
- * For every `.md` under the scanned surfaces, every inline link `[text](target)` whose target is
23
+ * For every `.md`/`.mdc` under the scanned surfaces, every inline link `[text](target)` whose target is
24
24
  * relative (not http(s):, not mailto:, not `#anchor`, not an absolute path) must exist on disk,
25
25
  * resolved against the LINKING FILE's own directory. A `#fragment` suffix is stripped before the
26
26
  * existence check — anchors are out of scope (no heading index here); the path half is not.
@@ -42,6 +42,7 @@
42
42
 
43
43
  import { readFileSync, readdirSync, statSync } from 'node:fs';
44
44
  import { join, dirname, resolve, relative, sep } from 'node:path';
45
+ import { isMainModule } from '../is-main-module.mjs';
45
46
 
46
47
  /**
47
48
  * Surfaces whose markdown is instruction, i.e. read and acted on.
@@ -52,8 +53,28 @@ import { join, dirname, resolve, relative, sep } from 'node:path';
52
53
  * `-/issues/N` renderer links now carved out by `SKIP_TARGET`, two of them genuinely dangling
53
54
  * links to PRDs that had been archived to the private Meta-Vault. Both classes were resolved
54
55
  * before the widening, so the widening itself lands at 0 findings.
56
+ *
57
+ * `.cursor/rules` joined in the 3ebf0e9d fix-pass, with its yield stated honestly: that surface
58
+ * ships in the npm tarball and is symlinked into consumer repos by `scripts/cursor-install.mjs`,
59
+ * and it carried two citations of `commands/{go,close}.md` after those files were folded into
60
+ * their skills. Measured 2026-09-17: `.cursor/rules/*.mdc` contain ZERO `[text](target)` links
61
+ * (`grep -rnoE '\[[^]]*\]\([^)]+\)' .cursor/rules/` → no matches), so the widening adds 9 files
62
+ * and 0 checked links today and would NOT by itself have caught that defect — those citations are
63
+ * INLINE-CODE (`` `commands/go.md` ``), which this checker deliberately skips (see DELIBERATE
64
+ * NON-CHECKS). It is a forward guard for the day a real link is authored there, not the catcher
65
+ * for the backticked-citation class; that class has no gate in this repo (recorded, not assumed).
66
+ */
67
+ export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', '.claude/rules', 'docs', '.cursor/rules']);
68
+
69
+ /**
70
+ * File extensions this checker treats as instruction markdown.
71
+ *
72
+ * `.mdc` is Cursor's own rule extension and is the ONLY reason `.cursor/rules`
73
+ * is scannable at all: every file there is `<nnn>-<name>.mdc`, so a `.md`-only
74
+ * filter would have added the directory to {@link SCAN_DIRS} and enumerated
75
+ * zero files — a widening that looks live and checks nothing.
55
76
  */
56
- export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', '.claude/rules', 'docs']);
77
+ export const MD_EXTENSIONS = Object.freeze(['.md', '.mdc']);
57
78
 
58
79
  /** Path segments that end the walk: vendored or machine-owned trees, never instruction. */
59
80
  export const PRUNE_DIRS = new Set(['node_modules', '.git', 'coverage', 'dist', '.pnpm']);
@@ -108,8 +129,8 @@ function fencedLineNumbers(text) {
108
129
  * and the check would report clean on the tree that carries the defect. This repo has the incident
109
130
  * on record (`.claude/rules/measurement-discipline.md` § "A `git grep` drift sweep cannot see
110
131
  * untracked files": a release sweep passed, then failed after the commit, from the same working
111
- * tree with no edit in between). The cost of the filesystem walk is that a gitignored `.md` under
112
- * these four directories would also be checked — there are none, and one would be a finding worth
132
+ * tree with no edit in between). The cost of the filesystem walk is that a gitignored markdown file
133
+ * under these directories would also be checked — there are none, and one would be a finding worth
113
134
  * seeing anyway.
114
135
  *
115
136
  * @param {string} repoRoot
@@ -125,7 +146,7 @@ export function listMarkdown(repoRoot, { dirs = SCAN_DIRS } = {}) {
125
146
  let entries;
126
147
  try { entries = readdirSync(abs, { recursive: true, withFileTypes: true }); } catch { continue; }
127
148
  for (const e of entries) {
128
- if (!e.isFile() || !e.name.endsWith('.md')) continue;
149
+ if (!e.isFile() || !MD_EXTENSIONS.some((ext) => e.name.endsWith(ext))) continue;
129
150
  // parentPath is absolute; make the record repo-relative with POSIX separators.
130
151
  const rel = relative(repoRoot, join(e.parentPath ?? abs, e.name));
131
152
  const posix = sep === '/' ? rel : rel.split(sep).join('/');
@@ -190,4 +211,4 @@ function main() {
190
211
  process.stdout.write(` PASS: ${checked} relative link(s) in ${files} markdown file(s) resolve\n`);
191
212
  }
192
213
 
193
- if (import.meta.url === `file://${process.argv[1]}`) main();
214
+ if (isMainModule(import.meta.url)) main();
@@ -100,9 +100,9 @@
100
100
 
101
101
  import { existsSync, readFileSync } from 'node:fs';
102
102
  import path from 'node:path';
103
- import { pathToFileURL } from 'node:url';
104
103
  import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
105
104
  import { forEachLine } from './markdown-fences.mjs';
105
+ import { isMainModule } from '../is-main-module.mjs';
106
106
 
107
107
  /** Documentation roots whose prose is treated as a claim about the repo. */
108
108
  export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', 'docs']);
@@ -461,7 +461,7 @@ export function runCheckSkillScriptPaths(pluginRoot, { strictSh = false } = {})
461
461
  return inspection.ok ? 0 : 1;
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 strictSh = process.argv.includes('--strict-sh');
467
467
  const args = process.argv.slice(2).filter((arg) => arg !== '--json' && arg !== '--strict-sh');
@@ -110,7 +110,7 @@
110
110
 
111
111
  import { readFileSync, readdirSync, statSync } from 'node:fs';
112
112
  import path from 'node:path';
113
- import { pathToFileURL } from 'node:url';
113
+ import { isMainModule } from '../is-main-module.mjs';
114
114
 
115
115
  /** Only this directory is scanned — a mutation in `scripts/` is production intent. */
116
116
  const SCAN_DIR = 'tests';
@@ -810,7 +810,7 @@ export function runCheckTestGitConfigTarget(pluginRoot) {
810
810
  return 0;
811
811
  }
812
812
 
813
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
813
+ const isMain =isMainModule(import.meta.url);
814
814
  if (isMain) {
815
815
  const argv = process.argv.slice(2);
816
816
  const flags = new Set(argv.filter((a) => a.startsWith('--')));
@@ -35,7 +35,7 @@
35
35
  import { readFileSync, readdirSync, statSync, existsSync, writeFileSync } from 'node:fs';
36
36
  import { join, extname, relative, sep } from 'node:path';
37
37
  import { execFileSync } from 'node:child_process';
38
- import { pathToFileURL } from 'node:url';
38
+ import { isMainModule } from '../is-main-module.mjs';
39
39
 
40
40
  // ---------------------------------------------------------------------------
41
41
  // File enumeration — git ls-files primary, recursive fs walk fallback.
@@ -535,7 +535,7 @@ export function runCheckUnicodeSafety(pluginRoot, opts = {}) {
535
535
 
536
536
  // CLI entry — only when executed directly, never on import (keeps the exports
537
537
  // safe to import from tests without triggering process.exit).
538
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
538
+ const isMain =isMainModule(import.meta.url);
539
539
  if (isMain) {
540
540
  const args = process.argv.slice(2);
541
541
  const flags = new Set(args.filter((a) => a.startsWith('--')));
@@ -104,10 +104,10 @@
104
104
  // Import-safety: importing this module MUST NOT execute anything — the isMain
105
105
  // guard at the bottom is the only side-effecting path.
106
106
 
107
- import { pathToFileURL } from 'node:url';
108
107
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
109
108
  import { spawnSync } from 'node:child_process';
110
109
  import path from 'node:path';
110
+ import { isMainModule } from '../is-main-module.mjs';
111
111
 
112
112
  // ---------------------------------------------------------------------------
113
113
  // Constants
@@ -938,7 +938,7 @@ export function runCheckUntrackedTestDeps(repoRoot) {
938
938
  return findings.length > 0 ? 1 : 0;
939
939
  }
940
940
 
941
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
941
+ const isMain =isMainModule(import.meta.url);
942
942
  if (isMain) {
943
943
  const root = process.argv[2];
944
944
  if (!root) {