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
@@ -166,6 +166,43 @@
166
166
  * both classes stay in `findings`, and either half collapsing to zero is itself
167
167
  * pinned by a test.
168
168
  *
169
+ * ## S5 `hand-keyed-learning-subject` — the prose-only DERIVATION
170
+ *
171
+ * S1-S4 all ask about a module or a key. S5 asks about a VALUE that prose tells
172
+ * an LLM to derive: `skills/evolve/references/evolve-analyze-mode.md` says the
173
+ * `effective-sizing` subject must come from `sizingSubject()` and that nobody may
174
+ * hand-concatenate it. Nothing enforced that. An analyzer run that writes
175
+ * `${session_type}-session-sizing` by hand — which is what it did before #1247,
176
+ * and which the helper's own profile-less branch still returns byte-identically —
177
+ * silently re-merges a 7-wave `ultradeep` session onto the 5-wave `deep` row, and
178
+ * no test, gate or checker notices.
179
+ *
180
+ * S5 therefore imports the helper (which is also what gives it a MECHANICAL
181
+ * consumer at all) and re-derives the subject of every written `effective-sizing`
182
+ * learning from the session record it names, reporting any mismatch. The
183
+ * comparison is the point: the check cannot drift from the helper, because it IS
184
+ * the helper.
185
+ *
186
+ * ### What it judges, and the measured reason it judges no more than that
187
+ *
188
+ * Only a subject already in the canonical `<...>-session-sizing` SHAPE is judged.
189
+ * Measured 2026-09-13 on this repo's live ledger (6 `effective-sizing` learnings):
190
+ * exactly ONE is canonically keyed (`deep-session-sizing`, and it is correct); the
191
+ * other five are free-form sentences (`deep session of 10 small follow-up
192
+ * issues`, `full-gate-workers-under-host-contention`, …). Judging those would put
193
+ * this class at 83% on day one — the broken instrument
194
+ * `.claude/rules/host-resources.md` § HR-101 forbids — and it would be judging
195
+ * prose STYLE, not a keying defect. A free-form subject cannot COLLIDE two
196
+ * profiles onto one row, which is the whole bug #1247 fixed.
197
+ *
198
+ * A learning whose `source_session` resolves to no session record is skipped and
199
+ * counted, never reported: the ledger is host-local and append-only, so an old
200
+ * learning routinely outlives the record it names, and reporting that would be a
201
+ * finding about ledger retention rather than about keying.
202
+ *
203
+ * Both ledger files are gitignored host-local telemetry. Absent → S5 is a silent
204
+ * no-op (CI sees no ledger), never a tool error.
205
+ *
169
206
  * ## Consumer scope, and why "prose-only" is a finding rather than an error
170
207
  *
171
208
  * Read sites are counted in `scripts/**` and `hooks/**` (`.mjs`/`.js`/`.cjs`),
@@ -228,7 +265,9 @@
228
265
 
229
266
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
230
267
  import path from 'node:path';
231
- import { pathToFileURL } from 'node:url';
268
+
269
+ import { sizingSubject } from '../learnings/sizing-subject.mjs';
270
+ import { isMainModule } from '../is-main-module.mjs';
232
271
 
233
272
  /** Documented config surface — every `yaml` fence in this file is a declaration. */
234
273
  const TEMPLATE_REL = 'docs/session-config-template.md';
@@ -331,6 +370,17 @@ const WIRING_DIRS = Object.freeze([
331
370
  ['hooks', Object.freeze(['.json', '.sh'])],
332
371
  ]);
333
372
 
373
+ /**
374
+ * Host-local telemetry the S5 subject-parity check reads. Both are gitignored
375
+ * (`.gitignore:55` `.orchestrator/metrics/*.jsonl`), so both are routinely
376
+ * ABSENT — that is a no-op, not an error. See the header, § S5.
377
+ */
378
+ const LEARNINGS_REL = path.join('.orchestrator', 'metrics', 'learnings.jsonl');
379
+ const SESSIONS_REL = path.join('.orchestrator', 'metrics', 'sessions.jsonl');
380
+
381
+ /** The canonical `effective-sizing` subject shape `sizingSubject()` produces. */
382
+ const SIZING_SUBJECT_RE = /-session-sizing$/;
383
+
334
384
  /**
335
385
  * The config-parser layer: the files a Session Config key must pass through to
336
386
  * become a runtime value. Signal S2 (see header) checks top-level keys against
@@ -372,7 +422,7 @@ const ALLOWLIST = Object.freeze({
372
422
  * @typedef {{
373
423
  * kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'allowlist-missing-reason'
374
424
  * | 'allowlist-stale' | 'orphaned-prose-module' | 'unreachable-library-module'
375
- * | 'coordinator-invoked-module'
425
+ * | 'coordinator-invoked-module' | 'hand-keyed-learning-subject'
376
426
  * | 'tool-error',
377
427
  * key: string,
378
428
  * message: string,
@@ -602,19 +652,57 @@ export function collectExportedSymbols(body) {
602
652
  return [...names];
603
653
  }
604
654
 
655
+ /**
656
+ * Re-export targets of a barrel: the specifiers of its `export * from '…'`
657
+ * (and `export * as ns from '…'`) lines.
658
+ *
659
+ * `collectExportedSymbols` cannot see these — a star re-export names no symbol —
660
+ * so a PURE barrel reads as `exports: []`. That does not merely hide the barrel:
661
+ * the S4 population predicate below requires at least one export, so the barrel
662
+ * never enters `unreachable` at all, and is therefore unavailable as the CLUSTER
663
+ * ROOT of the module it re-exports. The target is then reported as its own root.
664
+ *
665
+ * Measured 2026-09-16 on the live tree: `scripts/lib/worktree.mjs` is exactly
666
+ * `export * from './worktree/index.mjs';`, its only importers (`workspace.mjs`,
667
+ * `worktree-freshness.mjs`) are themselves unreachable — yet S4 reported
668
+ * `scripts/lib/worktree/index.mjs`, an interior file, instead of the cluster.
669
+ *
670
+ * Only the POPULATION is affected. Edge propagation already works: the BFS walks
671
+ * `mentions`, and a star re-export line is not a comment, so a barrel that IS
672
+ * reachable already marks its target reachable (measured the same day:
673
+ * `worktree.mjs` carries `index.mjs` in `mentions`).
674
+ *
675
+ * Anchored at column 0 like the export-symbol grammar above, so a docblock line
676
+ * (`* export * from …`) and an indented string never count.
677
+ *
678
+ * @param {string} body module source
679
+ * @returns {string[]} re-exported module specifiers, as written
680
+ */
681
+ export function collectReExportTargets(body) {
682
+ return [
683
+ ...body.matchAll(/^export\s+\*(?:\s+as\s+[A-Za-z0-9_$]+)?\s+from\s+['"]([^'"]+)['"]/gm),
684
+ ].map((match) => match[1]);
685
+ }
686
+
605
687
  /**
606
688
  * Whether a module is a CLI entrypoint rather than a library.
607
689
  *
608
690
  * An entrypoint is invoked by path (npm script, hook wiring, CI job), so having
609
691
  * no importer is its normal state and says nothing about being wired.
610
692
  *
693
+ * `isMainModule(` is part of the grammar because the #1371 sweep replaced ~50
694
+ * hand-written `process.argv[1] === import.meta.url` guards with the shared
695
+ * `scripts/lib/is-main-module.mjs` predicate. Without this alternative a swept
696
+ * CLI matches none of the other branches and silently reclassifies as a library
697
+ * candidate, so S4 reports it as an `unreachable-library-module` root.
698
+ *
611
699
  * @param {string} body module source
612
700
  * @returns {boolean}
613
701
  */
614
702
  export function isCliEntrypoint(body) {
615
703
  return (
616
704
  body.startsWith('#!') ||
617
- /import\.meta\.url\s*===|require\.main\s*===\s*module|process\.argv\[1\]/.test(body)
705
+ /import\.meta\.url\s*===|require\.main\s*===\s*module|process\.argv\[1\]|isMainModule\(/.test(body)
618
706
  );
619
707
  }
620
708
 
@@ -840,6 +928,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
840
928
  edgeOnly,
841
929
  entrypoint: isCliEntrypoint(body),
842
930
  exports: collectExportedSymbols(body),
931
+ // Star re-exports name no symbol, so `exports` is empty for a pure
932
+ // barrel — see collectReExportTargets for why that silently promoted an
933
+ // interior module to a root.
934
+ reExports: collectReExportTargets(body),
843
935
  // This file contributes NO edges — the S4 counterpart of the SELF_REL
844
936
  // exclusion the S1/S2 corpus already applies, and for the identical
845
937
  // reason. Every S4 `ALLOWLIST` key is a module path written here as a
@@ -848,7 +940,18 @@ export function collectUnreachableLibraryModules(pluginRoot) {
848
940
  // `allowlist-stale` — the check silently blinding itself to exactly the
849
941
  // module an operator flagged. Measured 2026-08-28 on the first S4
850
942
  // allowlist entry: 52 → 51 unreachable modules plus one bogus stale line.
851
- mentions: relative === SELF_REL ? new Set() : mentionedModuleTokens(lines),
943
+ mentions:
944
+ relative === SELF_REL
945
+ ? // This file blinds itself to its own ALLOWLIST literals (above) — but
946
+ // NOT to its own real imports. Dropping every edge here made a module
947
+ // this file genuinely `import`s (`learnings/sizing-subject.mjs`, the
948
+ // S5 check) still read as unreachable, which is the identical
949
+ // self-blinding the exclusion exists to prevent, in the other
950
+ // direction. Static import/export lines carry real edges; an
951
+ // ALLOWLIST key is never one, so scoping the scan to them keeps both
952
+ // properties.
953
+ mentionedModuleTokens(lines.filter((line) => /^\s*(?:import|export)\b/.test(line)))
954
+ : mentionedModuleTokens(lines),
852
955
  // Raw text, kept for the QUALIFIED (`dirname/base`) re-check in the
853
956
  // root filter below: `mentionedModuleTokens` strips the directory, so
854
957
  // a colliding basename can only be disambiguated against the body.
@@ -886,8 +989,15 @@ export function collectUnreachableLibraryModules(pluginRoot) {
886
989
  }
887
990
  }
888
991
 
992
+ // A module with NO public surface at all is not a finding — nothing can be
993
+ // wired to it. A pure `export * from` barrel HAS a surface (everything its
994
+ // target exports), so it belongs in the population: otherwise it cannot be
995
+ // the cluster root of the module it re-exports (collectReExportTargets).
889
996
  const unreachable = modules.filter(
890
- (module) => !reachable.has(module.relative) && !module.entrypoint && module.exports.length > 0,
997
+ (module) =>
998
+ !reachable.has(module.relative) &&
999
+ !module.entrypoint &&
1000
+ (module.exports.length > 0 || module.reExports.length > 0),
891
1001
  );
892
1002
  const unreachableSet = new Set(unreachable.map((module) => module.relative));
893
1003
  // Basename census, shared by the root filter below and the downgrade half
@@ -923,6 +1033,35 @@ export function collectUnreachableLibraryModules(pluginRoot) {
923
1033
  );
924
1034
  });
925
1035
 
1036
+ // A pure `export *` barrel enters the population above so it can HEAD its own
1037
+ // cluster, but reporting it needs one more condition. S3 already exempts this
1038
+ // exact shape by name (`autopilot-telemetry.mjs`, condition 4): a star
1039
+ // re-export has zero NAMED symbols, so every "does prose name one of its
1040
+ // exports?" test is vacuously FALSE — including S4's own coordinator-invoked
1041
+ // downgrade below, which iterates `module.exports`. A reported pure barrel
1042
+ // could therefore never be downgraded, only allowlisted: a finding with no
1043
+ // legitimate exit. The category split is the barrel's TARGET. A barrel over a
1044
+ // module that is itself unreachable heads a genuinely dead cluster and is
1045
+ // reported; a barrel over LIVE code is a backward-compat shim, which is a
1046
+ // different (and much weaker) finding than "nothing can reach this feature".
1047
+ // Measured 2026-09-16: without this split `scripts/lib/autopilot-telemetry.mjs`
1048
+ // — `export * from './autopilot/telemetry.mjs'`, target reachable, sole
1049
+ // importer a test — became a new permanent WARN.
1050
+ // Ceiling: a dead shim over live code is now invisible to S4 as well as to S3.
1051
+ // Revisit if a stale back-compat shim is ever confirmed to have outlived its
1052
+ // last importer unnoticed — that wants its own check, not a looser S4.
1053
+ const reportableRoots = roots.filter((module) => {
1054
+ if (module.exports.length > 0) return true;
1055
+ return module.reExports.some((spec) => {
1056
+ if (!spec.startsWith('.')) {
1057
+ // Aliased/bare specifier: fall back to the basename granularity this
1058
+ // file already documents as its named residual.
1059
+ return [...unreachableSet].some((rel) => path.basename(rel) === path.basename(spec));
1060
+ }
1061
+ return unreachableSet.has(path.normalize(path.join(path.dirname(module.relative), spec)));
1062
+ });
1063
+ });
1064
+
926
1065
  // Category split (see § Category split in the doc block above): an INSTRUCTION
927
1066
  // document that names both the module AND one of its exported symbols is an
928
1067
  // order addressed to a reader who will execute it — the same grammar
@@ -936,7 +1075,7 @@ export function collectUnreachableLibraryModules(pluginRoot) {
936
1075
  .map((file) => ({ relative: path.relative(pluginRoot, file), body: readFileSync(file, 'utf8') }));
937
1076
 
938
1077
  let coordinatorInvoked = 0;
939
- const findings = roots.map((module) => {
1078
+ const findings = reportableRoots.map((module) => {
940
1079
  // Docs write POSIX separators regardless of host; `path.relative` does not.
941
1080
  const relativePosix = module.relative.split(path.sep).join('/');
942
1081
  const qualified = relativePosix.split('/').slice(-2).join('/');
@@ -971,11 +1110,17 @@ export function collectUnreachableLibraryModules(pluginRoot) {
971
1110
  (token) => token !== module.base && [...unreachableSet].some((rel) => path.basename(rel) === token),
972
1111
  );
973
1112
  const tail = dragged.length > 0 ? `, and drags ${dragged.length} further unreachable module(s)` : '';
1113
+ // A pure barrel reports its re-export surface; naming "0 symbol(s)" there
1114
+ // would read as a checker bug rather than as the barrel it is.
1115
+ const surface =
1116
+ module.exports.length > 0
1117
+ ? `exports ${module.exports.length} symbol(s) (${module.exports.slice(0, 3).join(', ')})`
1118
+ : `re-exports ${module.reExports.length} module(s) (${module.reExports.slice(0, 3).join(', ')})`;
974
1119
  return /** @type {Finding} */ ({
975
1120
  kind: 'unreachable-library-module',
976
1121
  key: module.relative,
977
1122
  message:
978
- `exports ${module.exports.length} symbol(s) (${module.exports.slice(0, 3).join(', ')}) but no hook, ` +
1123
+ `${surface} but no hook, ` +
979
1124
  `npm script, CI job or husky stage reaches it — transitively${tail}. No instruction surface names ` +
980
1125
  'one of its exports either: wire it, delete it, or allowlist it with a reason',
981
1126
  });
@@ -985,13 +1130,107 @@ export function collectUnreachableLibraryModules(pluginRoot) {
985
1130
  findings,
986
1131
  scanned: {
987
1132
  modules: modules.length,
988
- roots: roots.length,
1133
+ roots: reportableRoots.length,
989
1134
  unreachable: unreachable.length,
990
1135
  coordinatorInvoked,
991
1136
  },
992
1137
  };
993
1138
  }
994
1139
 
1140
+ /**
1141
+ * Read a JSONL ledger, skipping blank and unparseable lines.
1142
+ *
1143
+ * A single truncated line (an interrupted append) must not blind the whole
1144
+ * check — the same fail-soft posture the rest of this file takes toward a
1145
+ * missing surface.
1146
+ *
1147
+ * @param {string} file absolute path
1148
+ * @returns {Record<string, unknown>[]}
1149
+ */
1150
+ function readJsonl(file) {
1151
+ if (!existsSync(file)) return [];
1152
+ /** @type {Record<string, unknown>[]} */
1153
+ const records = [];
1154
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
1155
+ const trimmed = line.trim();
1156
+ if (trimmed === '') continue;
1157
+ try {
1158
+ const parsed = JSON.parse(trimmed);
1159
+ if (parsed && typeof parsed === 'object') records.push(parsed);
1160
+ } catch {
1161
+ // truncated / partial append — skip the line, keep the census
1162
+ }
1163
+ }
1164
+ return records;
1165
+ }
1166
+
1167
+ /**
1168
+ * Signal S5 — an `effective-sizing` learning whose subject was hand-written
1169
+ * instead of derived from `sizingSubject()`.
1170
+ *
1171
+ * See the header § S5 for the defect, the deliberate narrowness of what is
1172
+ * judged, and the measured reason a free-form subject is not a finding.
1173
+ *
1174
+ * @param {string} pluginRoot absolute plugin root
1175
+ * @returns {{findings: Finding[], scanned: {judged: number, unattributed: number}}}
1176
+ */
1177
+ export function collectHandKeyedLearningSubjects(pluginRoot) {
1178
+ /** @type {Finding[]} */
1179
+ const findings = [];
1180
+ let judged = 0;
1181
+ let unattributed = 0;
1182
+
1183
+ const learnings = readJsonl(path.join(pluginRoot, LEARNINGS_REL));
1184
+ if (learnings.length === 0) return { findings, scanned: { judged, unattributed } };
1185
+
1186
+ // A session record can be addressed by either identity form (see
1187
+ // `.claude/rules/identity-and-locks.md`: raw UUID + semantic id), and a
1188
+ // learning's `source_session` carries whichever the writer had. Index both, so
1189
+ // a resolvable record is never mis-counted as unattributed.
1190
+ /** @type {Map<string, Record<string, unknown>>} */
1191
+ const sessionsById = new Map();
1192
+ for (const record of readJsonl(path.join(pluginRoot, SESSIONS_REL))) {
1193
+ for (const field of ['session_id', 'semantic_session_id']) {
1194
+ const id = record[field];
1195
+ if (typeof id === 'string' && id !== '' && !sessionsById.has(id)) sessionsById.set(id, record);
1196
+ }
1197
+ }
1198
+
1199
+ for (const learning of learnings) {
1200
+ if (learning.type !== 'effective-sizing') continue;
1201
+ const subject = learning.subject;
1202
+ if (typeof subject !== 'string' || !SIZING_SUBJECT_RE.test(subject)) continue;
1203
+
1204
+ const sourceSession = typeof learning.source_session === 'string' ? learning.source_session : '';
1205
+ const record = sessionsById.get(sourceSession);
1206
+ if (!record) {
1207
+ unattributed += 1;
1208
+ continue;
1209
+ }
1210
+
1211
+ judged += 1;
1212
+ const expected = sizingSubject({
1213
+ session_type: record.session_type,
1214
+ session_profile: record.session_profile,
1215
+ });
1216
+ if (subject === expected) continue;
1217
+
1218
+ findings.push({
1219
+ kind: 'hand-keyed-learning-subject',
1220
+ key: typeof learning.id === 'string' ? learning.id : subject,
1221
+ message:
1222
+ `effective-sizing learning keyed '${subject}', but sizingSubject() derives '${expected}' from ` +
1223
+ `${sourceSession} (session_type=${String(record.session_type)}, session_profile=` +
1224
+ `${record.session_profile === undefined ? 'absent' : String(record.session_profile)}) — the ` +
1225
+ 'subject was hand-concatenated instead of derived via ' +
1226
+ 'scripts/lib/learnings/sizing-subject.mjs, so two session shapes can silently share one row ' +
1227
+ '(GitLab #1247); re-key the learning and derive via the helper',
1228
+ });
1229
+ }
1230
+
1231
+ return { findings, scanned: { judged, unattributed } };
1232
+ }
1233
+
995
1234
  /**
996
1235
  * Run the full census.
997
1236
  *
@@ -1000,7 +1239,8 @@ export function collectUnreachableLibraryModules(pluginRoot) {
1000
1239
  * ok: boolean,
1001
1240
  * summary: {declaredKeys: number, consumerFiles: number, unwired: number, allowlisted: number,
1002
1241
  * orphanedModules: number, unreachableModules: number,
1003
- * coordinatorInvokedModules: number},
1242
+ * coordinatorInvokedModules: number, handKeyedSubjects: number,
1243
+ * judgedSubjects: number},
1004
1244
  * sourcesScanned: string[],
1005
1245
  * findings: Finding[],
1006
1246
  * toolError: boolean,
@@ -1019,6 +1259,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
1019
1259
  orphanedModules: 0,
1020
1260
  unreachableModules: 0,
1021
1261
  coordinatorInvokedModules: 0,
1262
+ handKeyedSubjects: 0,
1263
+ judgedSubjects: 0,
1022
1264
  },
1023
1265
  /** @type {string[]} */
1024
1266
  sourcesScanned: [],
@@ -1036,6 +1278,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
1036
1278
  let orphans;
1037
1279
  /** @type {ReturnType<typeof collectUnreachableLibraryModules>} */
1038
1280
  let unreachable;
1281
+ /** @type {ReturnType<typeof collectHandKeyedLearningSubjects>} */
1282
+ let handKeyed;
1039
1283
  try {
1040
1284
  declared = collectDeclaredKeys(pluginRoot);
1041
1285
  corpus = CONSUMER_DIRS.flatMap((dir) => walkCode(path.join(pluginRoot, dir)))
@@ -1054,6 +1298,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
1054
1298
  .join('\n');
1055
1299
  orphans = collectOrphanedProseModules(pluginRoot);
1056
1300
  unreachable = collectUnreachableLibraryModules(pluginRoot);
1301
+ handKeyed = collectHandKeyedLearningSubjects(pluginRoot);
1057
1302
  } catch (error) {
1058
1303
  result.toolError = true;
1059
1304
  findings.push({
@@ -1134,6 +1379,13 @@ export function inspectUnwiredFeatures(pluginRoot) {
1134
1379
  findings.push(finding);
1135
1380
  }
1136
1381
 
1382
+ // S5 — a written learning subject that prose said must come from the helper,
1383
+ // and did not. Not allowlistable: the fix is to re-key the record, and an
1384
+ // exemption would preserve exactly the collision #1247 removed.
1385
+ result.summary.handKeyedSubjects = handKeyed.findings.length;
1386
+ result.summary.judgedSubjects = handKeyed.scanned.judged;
1387
+ findings.push(...handKeyed.findings);
1388
+
1137
1389
  for (const key of Object.keys(ALLOWLIST).sort()) {
1138
1390
  if (flagged.has(key)) continue;
1139
1391
  findings.push({
@@ -1177,6 +1429,8 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
1177
1429
  orphanedModules,
1178
1430
  unreachableModules,
1179
1431
  coordinatorInvokedModules,
1432
+ handKeyedSubjects,
1433
+ judgedSubjects,
1180
1434
  } = inspection.summary;
1181
1435
 
1182
1436
  // S4 is a BACKLOG, not a per-run alarm: 50 findings on the live tree against
@@ -1215,14 +1469,15 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
1215
1469
  ` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
1216
1470
  `against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
1217
1471
  `${orphanedModules} prose-orphaned module(s), ${unreachableModules} unreachable module(s), ` +
1218
- `${coordinatorInvokedModules} coordinator-invoked module(s)`,
1472
+ `${coordinatorInvokedModules} coordinator-invoked module(s), ${handKeyedSubjects} hand-keyed ` +
1473
+ `learning subject(s) of ${judgedSubjects} judged`,
1219
1474
  );
1220
1475
  console.log('');
1221
1476
  console.log('Results: 1 passed, 0 failed');
1222
1477
  return 0;
1223
1478
  }
1224
1479
 
1225
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
1480
+ const isMain = isMainModule(import.meta.url);
1226
1481
  if (isMain) {
1227
1482
  const args = process.argv.slice(2);
1228
1483
  const pluginRoot = args.find((arg) => !arg.startsWith('-'));
@@ -72,8 +72,8 @@
72
72
 
73
73
  import { existsSync, readFileSync } from 'node:fs';
74
74
  import path from 'node:path';
75
- import { pathToFileURL } from 'node:url';
76
75
  import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
76
+ import { isMainModule } from '../is-main-module.mjs';
77
77
 
78
78
  /** Marker line inside a checker's own header — declares deliberate CLI-only status. */
79
79
  export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
@@ -243,7 +243,7 @@ export function runCheckValidatorRegistration(repoRoot) {
243
243
  return fail > 0 ? 1 : 0;
244
244
  }
245
245
 
246
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
246
+ const isMain =isMainModule(import.meta.url);
247
247
  if (isMain) {
248
248
  const root = process.argv[2];
249
249
  if (!root) {
@@ -193,8 +193,8 @@
193
193
 
194
194
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
195
195
  import path from 'node:path';
196
- import { pathToFileURL } from 'node:url';
197
196
  import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
197
+ import { isMainModule } from '../is-main-module.mjs';
198
198
 
199
199
  /** Directories whose content is scanned. Root-level `*.md` is added separately. */
200
200
  const SCAN_DIRS = Object.freeze([
@@ -906,7 +906,7 @@ export function runCheckVcsRepoFlag(pluginRoot) {
906
906
  return 0;
907
907
  }
908
908
 
909
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
909
+ const isMain =isMainModule(import.meta.url);
910
910
  if (isMain) {
911
911
  const argv = process.argv.slice(2);
912
912
  const flags = new Set(argv.filter((a) => a.startsWith('--')));
@@ -6,9 +6,12 @@
6
6
  * Catches vendoring bugs that are invisible at the source-of-truth layer but
7
7
  * become live footguns once synced into a target repo:
8
8
  *
9
- * - `paths:` frontmatter — `rule-loader.mjs` only understands `globs:`; a
10
- * `paths:` key is silently ignored, so the rule loads as always-on
11
- * instead of the intended glob-scoped subset.
9
+ * - `paths:` frontmatter in a `rules/` LIBRARY SOURCE — a vendoring-
10
+ * CONVENTION gate, not a loader-compatibility one. Since #795
11
+ * `rule-loader.mjs` accepts `paths:` as an alias for `globs:`, so such a
12
+ * rule IS glob-scoped; `globs:` stays the canonical form for a rule
13
+ * vendored out through the library (#742). See § Scope below — this probe
14
+ * never judges a repo's own consolidated `.claude/rules/` files.
12
15
  * - Missing provenance header — `rules-sync.mjs` detects "plugin-owned vs.
13
16
  * local override" purely by checking whether the first line starts with
14
17
  * `PLUGIN_HEADER_PREFIX`. A source file missing that header gets
@@ -31,6 +34,25 @@
31
34
  * module imports the already-exported `parseGlobsFrontmatter` from
32
35
  * `rule-loader.mjs` directly.
33
36
  *
37
+ * ## Scope: the `rules/` fleet library, never a repo's own `.claude/rules/`
38
+ *
39
+ * Every production caller feeds this module SOURCE files from the plugin's
40
+ * fleet library: `syncRules()` (`rules-sync.mjs`) validates
41
+ * `<pluginRoot>/rules/<relPath>` for each entry listed in `rules/_index.md`
42
+ * BEFORE writing the vendored copy into a consumer's `.claude/rules/` — the
43
+ * written target is never read back through the gate. The CLI's `--dir` is
44
+ * operator-supplied and means that same library.
45
+ *
46
+ * This is load-bearing for `paths-frontmatter`: a repo's own CONSOLIDATED rules
47
+ * under `.claude/rules/` are `paths:`-canonical by design, because Claude
48
+ * Code's native loader reads ONLY `paths:` and treats a rule without it as
49
+ * always-on (`validate/check-rules.mjs` #1108; `docs/rule-authoring.md`
50
+ * § Consolidated rules point 3). Measured 2026-09-16 in this repo: 10
51
+ * path-scoped rule files there, 9 of them `paths:`-only. Pointing `--dir` at
52
+ * such a tree would emit findings that, if obeyed, UNDO that consolidation —
53
+ * which is also why `check-rules.mjs` deliberately does not duplicate this
54
+ * probe for `.claude/rules/`.
55
+ *
34
56
  * @module validate-vendored-rules
35
57
  */
36
58
 
@@ -40,6 +62,7 @@ import { fileURLToPath } from 'node:url';
40
62
  import { execFileSync } from 'node:child_process';
41
63
  import { createRequire } from 'node:module';
42
64
  import { parseGlobsFrontmatter } from './rule-loader.mjs';
65
+ import { isMainModule } from './is-main-module.mjs';
43
66
 
44
67
  // Mirrors rules-sync.mjs's exported PLUGIN_HEADER_PREFIX (rules-sync.mjs
45
68
  // line ~13). NOT imported from there on purpose: rules-sync.mjs imports
@@ -478,10 +501,17 @@ export function validateRuleContent({ content, relPath, targetRoot = null, requi
478
501
  // `paths:`-only rule IS glob-scoped. The probe itself stays: it enforces the canonical
479
502
  // vendoring form, which is a convention gate, not a loader-compatibility gate. That
480
503
  // intent survives #795 and is the subject of the #742 fleet canonicalisation sweep.
504
+ // SCOPE (2026-09-16): this fires only over `rules/` library sources — the module doc's
505
+ // § Scope section names every caller. Do NOT re-point it at a repo's own
506
+ // `.claude/rules/`: those consolidated files are `paths:`-canonical and obeying this
507
+ // remedy there would undo the consolidation (docs/rule-authoring.md point 3).
481
508
  message:
482
509
  `${relPath}: frontmatter declares a top-level 'paths:' key. It is a recognized alias ` +
483
510
  `for 'globs:' (issue #795), so the rule does load glob-scoped — but 'globs:' is the ` +
484
- `canonical form for vendored rules. Migrate to 'globs:' (see issue #742).`,
511
+ `canonical form for a rule VENDORED OUT through the plugin's rules/ library, which is ` +
512
+ `this probe's only population. Migrate to 'globs:' (see issue #742). This does NOT ` +
513
+ `apply to a repo's own consolidated .claude/rules/ files — those are paths:-canonical ` +
514
+ `(Claude Code's native loader reads only 'paths:') and this gate never scans them.`,
485
515
  line: lineWithinFrontmatter(fm.startLine, fm.body, pathsMatch.index),
486
516
  });
487
517
  }
@@ -652,11 +682,7 @@ export function validateRulesDir({ dir, targetRoot = null, requireProvenance = f
652
682
 
653
683
  const __filename = fileURLToPath(import.meta.url);
654
684
 
655
- const isMain =
656
- typeof process !== 'undefined' &&
657
- process.argv[1] !== null &&
658
- process.argv[1] !== undefined &&
659
- resolve(process.argv[1]) === resolve(__filename);
685
+ const isMain =isMainModule(import.meta.url);
660
686
 
661
687
  /**
662
688
  * @param {{ ok: boolean, files: Array<{ file: string, violations: RuleViolation[] }>, errorCount: number, warnCount: number }} result
@@ -55,12 +55,12 @@ import {
55
55
  } from 'node:fs';
56
56
  import { homedir } from 'node:os';
57
57
  import { join, relative, isAbsolute } from 'node:path';
58
- import { pathToFileURL } from 'node:url';
59
58
 
60
59
  import { emitEvent, sessionAttribution } from './events.mjs';
61
60
  import { tryAcquireFileLock, releaseFileLock } from './file-lock.mjs';
62
61
  import { readLock, isLockLive } from './session-lock.mjs';
63
62
  import { resolveSubagentSidecar } from '../../hooks/_lib/subagent-paths.mjs';
63
+ import { isMainModule } from './is-main-module.mjs';
64
64
 
65
65
  const DEFAULT_INTERVAL_S = 2;
66
66
  const EVENTS_FILE_REL = '.orchestrator/metrics/events.jsonl';
@@ -998,6 +998,6 @@ function main() {
998
998
 
999
999
  // Run only when executed as a script — importing for unit tests must not parse
1000
1000
  // vitest's argv and exit 1.
1001
- if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
1001
+ if (isMainModule(import.meta.url)) {
1002
1002
  main();
1003
1003
  }
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { reapStaleLocks } from './lib/lock-reaper.mjs';
25
+ import { isMainModule } from './lib/is-main-module.mjs';
25
26
 
26
27
  // ---------------------------------------------------------------------------
27
28
  // Human-readable output
@@ -175,7 +176,7 @@ EXIT CODES
175
176
  // CLI guard — prevents execution during test-time imports.
176
177
  // ---------------------------------------------------------------------------
177
178
 
178
- if (import.meta.url === `file://${process.argv[1]}`) {
179
+ if (isMainModule(import.meta.url)) {
179
180
  main()
180
181
  .then((code) => process.exit(code))
181
182
  .catch((err) => {