session-orchestrator 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/.agents/skills/autopilot/SKILL.md +1 -0
  2. package/.agents/skills/bootstrap/SKILL.md +2 -0
  3. package/.agents/skills/brainstorm/SKILL.md +3 -0
  4. package/.agents/skills/close/SKILL.md +17 -0
  5. package/.agents/skills/debug/SKILL.md +2 -0
  6. package/.agents/skills/discovery/SKILL.md +2 -1
  7. package/.agents/skills/dispatcher/SKILL.md +2 -0
  8. package/.agents/skills/eli5/SKILL.md +2 -0
  9. package/.agents/skills/eval/SKILL.md +1 -0
  10. package/.agents/skills/evolve/SKILL.md +2 -1
  11. package/.agents/skills/go/SKILL.md +18 -0
  12. package/.agents/skills/grill/SKILL.md +2 -0
  13. package/.agents/skills/harness-audit/SKILL.md +16 -0
  14. package/.agents/skills/memory-cleanup/SKILL.md +1 -0
  15. package/.agents/skills/persona-panel/SKILL.md +1 -0
  16. package/.agents/skills/plan/SKILL.md +3 -1
  17. package/.agents/skills/portfolio/SKILL.md +17 -0
  18. package/.agents/skills/reconcile/SKILL.md +1 -0
  19. package/.agents/skills/release/SKILL.md +18 -0
  20. package/.agents/skills/repo-audit/SKILL.md +1 -0
  21. package/.agents/skills/spinout/SKILL.md +1 -0
  22. package/.agents/skills/sunset-review/SKILL.md +2 -0
  23. package/.agents/skills/test/SKILL.md +17 -0
  24. package/.agents/skills/ux-grill/SKILL.md +2 -0
  25. package/.claude-plugin/marketplace.json +1 -1
  26. package/.claude-plugin/plugin.json +1 -1
  27. package/.codex-plugin/plugin.json +1 -1
  28. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  29. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  30. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  31. package/.codex-plugin/skills/close/SKILL.md +3 -3
  32. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
  33. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
  34. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  35. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  36. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  37. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  38. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  39. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  40. package/.codex-plugin/skills/go/SKILL.md +3 -3
  41. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  42. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  43. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  44. package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
  45. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
  46. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  47. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  48. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  49. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  50. package/.codex-plugin/skills/release/SKILL.md +3 -3
  51. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  52. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  53. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  54. package/.codex-plugin/skills/test/SKILL.md +3 -3
  55. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  56. package/.cursor/commands/autopilot.md +4 -4
  57. package/.cursor/commands/bootstrap.md +5 -4
  58. package/.cursor/commands/brainstorm.md +5 -4
  59. package/.cursor/commands/close.md +4 -3
  60. package/.cursor/commands/convergence-monitoring.md +13 -0
  61. package/.cursor/commands/debug.md +4 -4
  62. package/.cursor/commands/discovery.md +4 -4
  63. package/.cursor/commands/dispatcher.md +4 -4
  64. package/.cursor/commands/eli5.md +4 -4
  65. package/.cursor/commands/eval.md +4 -4
  66. package/.cursor/commands/evolve.md +4 -4
  67. package/.cursor/commands/go.md +4 -3
  68. package/.cursor/commands/grill.md +4 -4
  69. package/.cursor/commands/harness-audit.md +3 -3
  70. package/.cursor/commands/memory-cleanup.md +4 -4
  71. package/.cursor/commands/npm-publish.md +13 -0
  72. package/.cursor/commands/persona-panel.md +4 -4
  73. package/.cursor/commands/plan.md +5 -4
  74. package/.cursor/commands/portfolio.md +3 -3
  75. package/.cursor/commands/reconcile.md +4 -4
  76. package/.cursor/commands/release.md +4 -3
  77. package/.cursor/commands/repo-audit.md +4 -4
  78. package/.cursor/commands/spinout.md +4 -4
  79. package/.cursor/commands/sunset-review.md +4 -4
  80. package/.cursor/commands/test.md +3 -3
  81. package/.cursor/commands/ux-grill.md +4 -4
  82. package/.cursor/rules/010-session-workflow.mdc +2 -2
  83. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  84. package/.cursor/skills/close/SKILL.md +13 -0
  85. package/.cursor/skills/debug/SKILL.md +0 -1
  86. package/.cursor/skills/discovery/SKILL.md +0 -1
  87. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  88. package/.cursor/skills/eli5/SKILL.md +0 -1
  89. package/.cursor/skills/evolve/SKILL.md +0 -1
  90. package/.cursor/skills/go/SKILL.md +13 -0
  91. package/.cursor/skills/grill/SKILL.md +0 -1
  92. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  93. package/.cursor/skills/portfolio/SKILL.md +12 -0
  94. package/.cursor/skills/release/SKILL.md +13 -0
  95. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  96. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  97. package/.cursor/skills/test/SKILL.md +12 -0
  98. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  99. package/.cursor-plugin/plugin.json +1 -1
  100. package/.orchestrator/policy/blocked-commands.json +1 -1
  101. package/AGENTS.md +1 -1
  102. package/CHANGELOG.md +61 -0
  103. package/README.md +11 -9
  104. package/commands/session.md +10 -0
  105. package/docs/ci-setup.md +53 -0
  106. package/docs/codex-setup.md +1 -1
  107. package/docs/components.md +11 -6
  108. package/docs/events-schema.md +4 -1
  109. package/docs/install.md +16 -0
  110. package/docs/persona-panel.md +1 -1
  111. package/docs/pi-setup.md +1 -1
  112. package/docs/rule-authoring.md +83 -14
  113. package/docs/scope-collision-guard.md +2 -0
  114. package/docs/session-config-reference.md +6 -4
  115. package/hooks/_lib/hook-import-set.json +46 -6
  116. package/hooks/_lib/subagent-paths.mjs +15 -0
  117. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/on-session-end.mjs +14 -2
  121. package/hooks/on-stop.mjs +43 -1
  122. package/hooks/post-bash-write-verify.mjs +3 -0
  123. package/hooks/pre-auq-clarity.mjs +3 -0
  124. package/hooks/pre-bash-issue-budget.mjs +103 -17
  125. package/hooks/pre-task-scope-disjoint.mjs +152 -3
  126. package/hooks/skill-invocation-telemetry.mjs +2 -1
  127. package/package.json +2 -1
  128. package/pi/prompts/autopilot.md +3 -3
  129. package/pi/prompts/bootstrap.md +3 -3
  130. package/pi/prompts/brainstorm.md +3 -3
  131. package/pi/prompts/close.md +2 -2
  132. package/pi/prompts/convergence-monitoring.md +11 -0
  133. package/pi/prompts/debug.md +3 -3
  134. package/pi/prompts/discovery.md +3 -3
  135. package/pi/prompts/dispatcher.md +3 -3
  136. package/pi/prompts/eli5.md +3 -3
  137. package/pi/prompts/eval.md +3 -3
  138. package/pi/prompts/evolve.md +3 -3
  139. package/pi/prompts/go.md +2 -2
  140. package/pi/prompts/grill.md +3 -3
  141. package/pi/prompts/harness-audit.md +2 -3
  142. package/pi/prompts/memory-cleanup.md +3 -3
  143. package/pi/prompts/npm-publish.md +11 -0
  144. package/pi/prompts/persona-panel.md +3 -3
  145. package/pi/prompts/plan.md +3 -3
  146. package/pi/prompts/portfolio.md +2 -2
  147. package/pi/prompts/reconcile.md +3 -3
  148. package/pi/prompts/release.md +3 -3
  149. package/pi/prompts/repo-audit.md +3 -4
  150. package/pi/prompts/session.md +1 -1
  151. package/pi/prompts/spinout.md +3 -3
  152. package/pi/prompts/sunset-review.md +3 -3
  153. package/pi/prompts/templates-ack.md +1 -1
  154. package/pi/prompts/test.md +3 -3
  155. package/pi/prompts/ux-grill.md +3 -3
  156. package/scripts/archive-closed-prds.mjs +2 -2
  157. package/scripts/auq-audit.mjs +2 -3
  158. package/scripts/backfill-abandoned-sessions.mjs +57 -3
  159. package/scripts/backfill-evidence-digest.mjs +2 -1
  160. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  161. package/scripts/check-package-manager.mjs +2 -2
  162. package/scripts/ci/assert-vitest-green.mjs +2 -1
  163. package/scripts/emit-session.mjs +2 -3
  164. package/scripts/export-hw-learnings.mjs +2 -1
  165. package/scripts/express-path.mjs +1 -1
  166. package/scripts/gc-stale-worktrees.mjs +2 -1
  167. package/scripts/generate-codex-skills.mjs +48 -4
  168. package/scripts/generate-cursor-adapter.mjs +173 -9
  169. package/scripts/generate-hook-import-set.mjs +12 -27
  170. package/scripts/generate-pi-prompts.mjs +183 -13
  171. package/scripts/github-protection-audit.mjs +2 -3
  172. package/scripts/lib/agent-frontmatter.mjs +23 -1
  173. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  174. package/scripts/lib/command-blocker.mjs +133 -5
  175. package/scripts/lib/config/drift-check.mjs +19 -0
  176. package/scripts/lib/convergence-monitor.mjs +2 -2
  177. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  178. package/scripts/lib/description-surface.mjs +2 -5
  179. package/scripts/lib/dispatcher/cli.mjs +2 -1
  180. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  181. package/scripts/lib/fetch-baseline.mjs +3 -8
  182. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  183. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  184. package/scripts/lib/instruction-budget-guard.mjs +186 -46
  185. package/scripts/lib/is-main-module.mjs +82 -0
  186. package/scripts/lib/locks/index.mjs +32 -25
  187. package/scripts/lib/maintenance-due-banner.mjs +69 -3
  188. package/scripts/lib/peer-discovery.mjs +2 -5
  189. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  190. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
  191. package/scripts/lib/rules-sync.mjs +2 -5
  192. package/scripts/lib/scope-echo.mjs +392 -7
  193. package/scripts/lib/session-close-backfill.mjs +58 -6
  194. package/scripts/lib/state-md.mjs +84 -3
  195. package/scripts/lib/sunset/walker.mjs +31 -4
  196. package/scripts/lib/tests-src-ratio.mjs +2 -6
  197. package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
  198. package/scripts/lib/user-invocable-skills.mjs +185 -0
  199. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  200. package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
  201. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  202. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  203. package/scripts/lib/validate/check-entry-guard.mjs +366 -0
  204. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  205. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  206. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  207. package/scripts/lib/validate/check-skill-links.mjs +27 -6
  208. package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
  209. package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
  210. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  211. package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
  212. package/scripts/lib/validate/check-unwired-features.mjs +91 -7
  213. package/scripts/lib/validate/check-validator-registration.mjs +2 -2
  214. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  215. package/scripts/lib/validate-vendored-rules.mjs +35 -9
  216. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  217. package/scripts/lock-reaper.mjs +2 -1
  218. package/scripts/materialize-wave-scope.mjs +87 -4
  219. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  220. package/scripts/migrate-vault-paths.mjs +2 -3
  221. package/scripts/release.mjs +80 -35
  222. package/scripts/relocate-vault-corpus.mjs +2 -3
  223. package/scripts/repair-invalid-sessions.mjs +2 -2
  224. package/scripts/session-shape.mjs +2 -2
  225. package/scripts/site-numbers.mjs +35 -11
  226. package/scripts/sweep-expired-rules.mjs +216 -0
  227. package/scripts/validate-plugin.mjs +9 -0
  228. package/scripts/vault-consolidate.mjs +2 -2
  229. package/scripts/vault-mirror.mjs +2 -3
  230. package/scripts/wave-scope-binding.mjs +2 -3
  231. package/skills/_shared/bootstrap-gate.md +1 -1
  232. package/skills/_shared/monitor-patterns.md +1 -1
  233. package/skills/_shared/research-evidence.md +53 -0
  234. package/skills/_shared/state-ownership.md +3 -0
  235. package/skills/autopilot/SKILL.md +58 -4
  236. package/skills/bootstrap/SKILL.md +51 -1
  237. package/skills/brainstorm/SKILL.md +16 -0
  238. package/skills/claude-md-drift-check/checker.mjs +49 -11
  239. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  240. package/skills/debug/SKILL.md +10 -0
  241. package/skills/discovery/SKILL.md +24 -1
  242. package/skills/discovery/probes-session.md +2 -2
  243. package/skills/dispatcher/SKILL.md +38 -7
  244. package/skills/eli5/SKILL.md +11 -0
  245. package/skills/eval/SKILL.md +14 -0
  246. package/skills/evolve/SKILL.md +8 -1
  247. package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
  248. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  249. package/skills/grill/SKILL.md +19 -0
  250. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  251. package/skills/hook-development/SKILL.md +46 -41
  252. package/skills/memory-cleanup/SKILL.md +7 -0
  253. package/skills/npm-publish/SKILL.md +1 -1
  254. package/skills/persona-panel/SKILL.md +56 -1
  255. package/skills/persona-panel/persona-format.md +1 -1
  256. package/skills/plan/SKILL.md +28 -1
  257. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  258. package/skills/reconcile/SKILL.md +10 -0
  259. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  260. package/skills/repo-audit/SKILL.md +7 -0
  261. package/skills/session-end/plan-verification.md +2 -2
  262. package/skills/session-plan/SKILL.md +1 -1
  263. package/skills/session-start/SKILL.md +5 -4
  264. package/skills/session-start/phase-8-5-express-path.md +6 -6
  265. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  266. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  267. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  268. package/skills/spinout/SKILL.md +12 -1
  269. package/skills/sunset-review/SKILL.md +13 -0
  270. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  271. package/skills/ux-grill/SKILL.md +19 -1
  272. package/skills/wave-executor/SKILL.md +7 -4
  273. package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
  274. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  275. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  276. package/commands/autopilot.md +0 -80
  277. package/commands/bootstrap.md +0 -56
  278. package/commands/brainstorm.md +0 -48
  279. package/commands/debug.md +0 -36
  280. package/commands/discovery.md +0 -32
  281. package/commands/dispatcher.md +0 -59
  282. package/commands/eli5.md +0 -33
  283. package/commands/eval.md +0 -28
  284. package/commands/evolve.md +0 -10
  285. package/commands/grill.md +0 -45
  286. package/commands/memory-cleanup.md +0 -26
  287. package/commands/persona-panel.md +0 -121
  288. package/commands/plan.md +0 -15
  289. package/commands/reconcile.md +0 -23
  290. package/commands/repo-audit.md +0 -24
  291. package/commands/spinout.md +0 -15
  292. package/commands/sunset-review.md +0 -27
  293. package/commands/ux-grill.md +0 -51
@@ -265,9 +265,9 @@
265
265
 
266
266
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
267
267
  import path from 'node:path';
268
- import { pathToFileURL } from 'node:url';
269
268
 
270
269
  import { sizingSubject } from '../learnings/sizing-subject.mjs';
270
+ import { isMainModule } from '../is-main-module.mjs';
271
271
 
272
272
  /** Documented config surface — every `yaml` fence in this file is a declaration. */
273
273
  const TEMPLATE_REL = 'docs/session-config-template.md';
@@ -652,19 +652,57 @@ export function collectExportedSymbols(body) {
652
652
  return [...names];
653
653
  }
654
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
+
655
687
  /**
656
688
  * Whether a module is a CLI entrypoint rather than a library.
657
689
  *
658
690
  * An entrypoint is invoked by path (npm script, hook wiring, CI job), so having
659
691
  * no importer is its normal state and says nothing about being wired.
660
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
+ *
661
699
  * @param {string} body module source
662
700
  * @returns {boolean}
663
701
  */
664
702
  export function isCliEntrypoint(body) {
665
703
  return (
666
704
  body.startsWith('#!') ||
667
- /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)
668
706
  );
669
707
  }
670
708
 
@@ -890,6 +928,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
890
928
  edgeOnly,
891
929
  entrypoint: isCliEntrypoint(body),
892
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),
893
935
  // This file contributes NO edges — the S4 counterpart of the SELF_REL
894
936
  // exclusion the S1/S2 corpus already applies, and for the identical
895
937
  // reason. Every S4 `ALLOWLIST` key is a module path written here as a
@@ -947,8 +989,15 @@ export function collectUnreachableLibraryModules(pluginRoot) {
947
989
  }
948
990
  }
949
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).
950
996
  const unreachable = modules.filter(
951
- (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),
952
1001
  );
953
1002
  const unreachableSet = new Set(unreachable.map((module) => module.relative));
954
1003
  // Basename census, shared by the root filter below and the downgrade half
@@ -984,6 +1033,35 @@ export function collectUnreachableLibraryModules(pluginRoot) {
984
1033
  );
985
1034
  });
986
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
+
987
1065
  // Category split (see § Category split in the doc block above): an INSTRUCTION
988
1066
  // document that names both the module AND one of its exported symbols is an
989
1067
  // order addressed to a reader who will execute it — the same grammar
@@ -997,7 +1075,7 @@ export function collectUnreachableLibraryModules(pluginRoot) {
997
1075
  .map((file) => ({ relative: path.relative(pluginRoot, file), body: readFileSync(file, 'utf8') }));
998
1076
 
999
1077
  let coordinatorInvoked = 0;
1000
- const findings = roots.map((module) => {
1078
+ const findings = reportableRoots.map((module) => {
1001
1079
  // Docs write POSIX separators regardless of host; `path.relative` does not.
1002
1080
  const relativePosix = module.relative.split(path.sep).join('/');
1003
1081
  const qualified = relativePosix.split('/').slice(-2).join('/');
@@ -1032,11 +1110,17 @@ export function collectUnreachableLibraryModules(pluginRoot) {
1032
1110
  (token) => token !== module.base && [...unreachableSet].some((rel) => path.basename(rel) === token),
1033
1111
  );
1034
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(', ')})`;
1035
1119
  return /** @type {Finding} */ ({
1036
1120
  kind: 'unreachable-library-module',
1037
1121
  key: module.relative,
1038
1122
  message:
1039
- `exports ${module.exports.length} symbol(s) (${module.exports.slice(0, 3).join(', ')}) but no hook, ` +
1123
+ `${surface} but no hook, ` +
1040
1124
  `npm script, CI job or husky stage reaches it — transitively${tail}. No instruction surface names ` +
1041
1125
  'one of its exports either: wire it, delete it, or allowlist it with a reason',
1042
1126
  });
@@ -1046,7 +1130,7 @@ export function collectUnreachableLibraryModules(pluginRoot) {
1046
1130
  findings,
1047
1131
  scanned: {
1048
1132
  modules: modules.length,
1049
- roots: roots.length,
1133
+ roots: reportableRoots.length,
1050
1134
  unreachable: unreachable.length,
1051
1135
  coordinatorInvoked,
1052
1136
  },
@@ -1393,7 +1477,7 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
1393
1477
  return 0;
1394
1478
  }
1395
1479
 
1396
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
1480
+ const isMain = isMainModule(import.meta.url);
1397
1481
  if (isMain) {
1398
1482
  const args = process.argv.slice(2);
1399
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) => {
@@ -23,10 +23,11 @@
23
23
  */
24
24
 
25
25
  import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
26
- import { resolve } from 'node:path';
27
- import { fileURLToPath } from 'node:url';
26
+ import { dirname, resolve } from 'node:path';
28
27
  import { writeJsonAtomicSync } from './lib/io.mjs';
28
+ import { isMainModule } from './lib/is-main-module.mjs';
29
29
  import { isPeerRecordId } from './lib/scope-gate.mjs';
30
+ import { SCOPE_MATERIALIZED_EVENT, scopeDigest } from './lib/scope-echo.mjs';
30
31
  import {
31
32
  MANIFEST_SESSION_KEYS,
32
33
  manifestSessionBinding,
@@ -479,6 +480,83 @@ export function materializeWaveScope(records, {
479
480
  return { aggregatePath, perAgentPaths, removedOrphans: removed, retainedOrphans: retained };
480
481
  }
481
482
 
483
+ /**
484
+ * Is the SEND-side half of the #1092 chain installed on this platform at all?
485
+ *
486
+ * The send-side record only exists because a `PreToolUse` hook with matcher
487
+ * `Agent` observes the dispatch. `hooks-codex.json` / `hooks-cursor.json` /
488
+ * `hooks-pi.json` deliberately carry no such entry (those platforms have no
489
+ * `Agent` dispatch tool — `DOCUMENTED_ASYMMETRIES`), so on them a MISSING
490
+ * dispatch record is not evidence of a missing injection. Recording the answer
491
+ * beside the wave is what lets `--verify` tell those two apart later instead of
492
+ * re-deriving a platform fact from an absence.
493
+ *
494
+ * Fail-CLOSED on any read error: an unreadable hooks file means the guard cannot
495
+ * be shown to be installed, and claiming observability we cannot prove is the
496
+ * direction that produces false `injection-missing` accusations.
497
+ *
498
+ * @param {string} [pluginRoot]
499
+ * @param {typeof readFileSync} [readFile]
500
+ * @returns {boolean}
501
+ */
502
+ export function transportObservable(pluginRoot = resolve(import.meta.dirname, '..'), readFile = readFileSync) {
503
+ try {
504
+ const parsed = JSON.parse(readFile(resolve(pluginRoot, 'hooks', 'hooks.json'), 'utf8'));
505
+ const entries = parsed?.hooks?.PreToolUse;
506
+ if (!Array.isArray(entries)) return false;
507
+ return entries.some((entry) => typeof entry?.matcher === 'string'
508
+ && entry.matcher.split('|').map((m) => m.trim()).includes('Agent'));
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+
514
+ /**
515
+ * One `orchestrator.wave_dispatch.scope_materialized` record per wave (#1092) —
516
+ * the DEGRADATION half of the chain.
517
+ *
518
+ * It runs here, and not in the hook, precisely because this command runs on
519
+ * EVERY platform while the hook runs on one. Without it a wave with zero
520
+ * `scope_checked` records is ambiguous between "the coordinator skipped the
521
+ * manifest step" (the #1020 failure this whole chain exists for) and "this
522
+ * platform has no dispatch hook".
523
+ *
524
+ * `repoRoot` is the state directory's PARENT, never `process.cwd()`: the state
525
+ * dir is the artefact this command owns, and a caller invoking it from a
526
+ * subdirectory (or a test from the repo root against a `$TMPDIR` state dir)
527
+ * must not write its record into a different repo's ledger.
528
+ *
529
+ * Best-effort in both directions and SILENT on failure: the corpus pins
530
+ * byte-empty stderr on this command's success path
531
+ * (`tests/integration/wave-scope-producer.test.mjs`), and a telemetry write must
532
+ * never be the thing that fails a materialization.
533
+ *
534
+ * @param {{stateDir: string, wave: number, records: Array<{id: string, files: string[]}>}} params
535
+ * @returns {Promise<void>}
536
+ */
537
+ async function emitScopeMaterialized({ stateDir, wave, records }) {
538
+ try {
539
+ const repoRoot = dirname(resolve(stateDir));
540
+ const agentRecords = records.filter((r) => !isPeerRecordId(r.id));
541
+ const digests = new Set();
542
+ for (const record of agentRecords) {
543
+ if (Array.isArray(record.files) && record.files.length > 0) digests.add(scopeDigest(record.files));
544
+ }
545
+ const { emitEvent, sessionAttribution } = await import('./lib/events.mjs');
546
+ await emitEvent(
547
+ SCOPE_MATERIALIZED_EVENT,
548
+ {
549
+ wave,
550
+ agent_count: agentRecords.length,
551
+ digest_count: digests.size,
552
+ transport_observable: transportObservable(),
553
+ ...sessionAttribution(repoRoot),
554
+ },
555
+ { repoRoot },
556
+ );
557
+ } catch { /* observability is best-effort — it never fails a materialization */ }
558
+ }
559
+
482
560
  /**
483
561
  * @param {string} message
484
562
  * @param {number} code
@@ -536,6 +614,12 @@ export function main() {
536
614
  ? `${JSON.stringify({ ok: true, aggregatePath, perAgentPaths, removedOrphans, retainedOrphans })}\n`
537
615
  : `${aggregatePath}\n`,
538
616
  );
617
+
618
+ // Fire-and-forget AFTER stdout, so the telemetry append can never reorder or
619
+ // delay this command's own contract. Node drains the pending append before
620
+ // exiting because nothing here calls `process.exit()`; the `.catch` keeps an
621
+ // unhandled rejection from turning observability into an exit code.
622
+ emitScopeMaterialized({ stateDir: args.stateDir, wave: args.wave, records }).catch(() => {});
539
623
  } catch (error) {
540
624
  if (error instanceof InputError) {
541
625
  fail(error.message, 1);
@@ -549,7 +633,6 @@ export function main() {
549
633
  }
550
634
  }
551
635
 
552
- const invokedPath = process.argv[1] ? resolve(process.argv[1]) : null;
553
- if (invokedPath === fileURLToPath(import.meta.url)) {
636
+ if (isMainModule(import.meta.url)) {
554
637
  main();
555
638
  }
@@ -41,6 +41,7 @@
41
41
  import { readFileSync, writeFileSync, renameSync, copyFileSync, existsSync } from 'node:fs';
42
42
  import { join, dirname, basename } from 'node:path';
43
43
  import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
44
+ import { isMainModule } from './lib/is-main-module.mjs';
44
45
 
45
46
  const DEFAULT_FILE = '.orchestrator/metrics/sessions.jsonl';
46
47
 
@@ -440,7 +441,7 @@ async function main() {
440
441
 
441
442
  // Run main() only when this file is invoked directly as a CLI, not when imported.
442
443
  // Prevents process.exit during test-time imports (#368).
443
- if (import.meta.url === `file://${process.argv[1]}`) {
444
+ if (isMainModule(import.meta.url)) {
444
445
  main().catch((err) => {
445
446
  process.stderr.write(`migrate-sessions-jsonl: unexpected error: ${err?.stack ?? err}\n`);
446
447
  process.exit(2);
@@ -50,7 +50,6 @@
50
50
 
51
51
  import { promises as fs, existsSync } from 'node:fs';
52
52
  import { spawnSync } from 'node:child_process';
53
- import { pathToFileURL } from 'node:url';
54
53
  import path from 'node:path';
55
54
  import os from 'node:os';
56
55
  import {
@@ -58,6 +57,7 @@ import {
58
57
  VAULT_MIGRATION_RULES_PATH,
59
58
  } from './lib/vault-migration-rules.mjs';
60
59
  import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
60
+ import { isMainModule } from './lib/is-main-module.mjs';
61
61
 
62
62
  // ---------------------------------------------------------------------------
63
63
  // Constants
@@ -773,8 +773,7 @@ function emit(opts, rec) {
773
773
  // importing the module is never a hard error regardless of how the importer runs.
774
774
  // ---------------------------------------------------------------------------
775
775
 
776
- const invokedDirectly =
777
- process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
776
+ const invokedDirectly =isMainModule(import.meta.url);
778
777
 
779
778
  if (invokedDirectly) {
780
779
  main().catch((err) => {
@@ -356,7 +356,7 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
356
356
  'templates/python-uv/tests/test_sanity.py',
357
357
  ]);
358
358
 
359
- // `commands/release.md` quotes the `npm view` OUTPUT that proves the 3.18.0 gap,
359
+ // `skills/release/SKILL.md` quotes the `npm view` OUTPUT that proves the 3.18.0 gap,
360
360
  // dated at the line. Bumping it would destroy the evidence it exists to carry —
361
361
  // the registry state on that date is the whole point of the paragraph.
362
362
  //
@@ -384,7 +384,7 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
384
384
  // September 10 campaign snapshot: inputs and receipt reproduce the dated 4.3-planned
385
385
  // render made while 4.2.0 was current. Exact files only; neighboring marketing
386
386
  // remains checked. Do not bump receipt props without a newly reviewed render.
387
- export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|commands\/release\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
387
+ export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
388
388
 
389
389
  /** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
390
390
  export function checkLeakage(lines) {
@@ -908,6 +908,11 @@ export function evaluateCiRow(ci) {
908
908
  * "we could not read the mirror" is not "the mirror is green" (the fail-closed
909
909
  * house rule at the top of this file).
910
910
  *
911
+ * This function only judges the `ci` reading it is handed — it is
912
+ * `evaluateCiPreflightRows` (the caller, see its own docblock) that fetches
913
+ * `ci` for the commit actually being released, rather than whichever HEAD the
914
+ * GitHub mirror itself reports.
915
+ *
911
916
  * @param {string|undefined} repoSpec — `resolveRepoSpec({vcs:'github'})`, undefined when no github remote resolves
912
917
  * @param {null | {status?: string, failingJobName?: string, degraded?: string}} ci
913
918
  * @returns {{ok: boolean, detail: string}}
@@ -969,6 +974,62 @@ function readPackageVersion(repoRoot) {
969
974
  return JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8')).version;
970
975
  }
971
976
 
977
+ /**
978
+ * Both CI preflight rows (`ci-green-on-head` + `ci-green-on-head-github`) for
979
+ * ONE commit — the commit actually about to be released — never whichever
980
+ * HEAD the queried platform happens to report.
981
+ *
982
+ * Before this function existed, neither `checkCiStatus` call below passed
983
+ * `sha`, so the GitHub branch fell back to `commits/HEAD` — the MIRROR's own
984
+ * default-branch head, which is the release commit only once `head-pushed-github`
985
+ * has ALREADY proven `github/main == local HEAD`. A release cut before that
986
+ * push landed asked GitHub about a commit that was never pushed and could read
987
+ * green for work GitHub has not seen at all (measured 2026-09-16 for
988
+ * `3ebf0e9d`). The GitLab branch was already correct by default (`deps.sha ??
989
+ * getHeadSha(repoRoot)`), so only the two call sites below needed the fix —
990
+ * see `checkCiStatus`'s own docblock in `ci-status-banner.mjs` for the `sha`
991
+ * contract (#1332).
992
+ *
993
+ * Factored out of `preflight()` as its own async, DI-testable unit:
994
+ * `checkCiStatus` reaches the network by default, and `preflight()`'s
995
+ * surrounding checks (git, npm) are not test-doubled, so this is the seam
996
+ * through which the `sha` wiring can be unit-tested without spinning up a
997
+ * full fixture release.
998
+ *
999
+ * @param {string} repoRoot
1000
+ * @param {string} head - full hex commit id of the commit being released (the
1001
+ * local HEAD `preflight()` already computed for the remote-parity rows).
1002
+ * @param {{
1003
+ * skipCi?: boolean,
1004
+ * checkCiStatus?: Function,
1005
+ * resolveRepoSpec?: (opts: { repoRoot: string, vcs: 'github' }) => string|undefined,
1006
+ * }} [deps]
1007
+ * @returns {Promise<{ gitlab: {ok: boolean, detail: string}, github: {ok: boolean, detail: string} }>}
1008
+ */
1009
+ export async function evaluateCiPreflightRows(repoRoot, head, {
1010
+ skipCi = false,
1011
+ checkCiStatus: checkCiStatusDep,
1012
+ resolveRepoSpec: resolveRepoSpecDep = resolveRepoSpec,
1013
+ } = {}) {
1014
+ if (skipCi) {
1015
+ const skipped = { ok: true, detail: 'SKIPPED via --skip-ci' };
1016
+ return { gitlab: skipped, github: skipped };
1017
+ }
1018
+ const checkCiStatusImpl = checkCiStatusDep ?? (await import('./lib/ci-status-banner.mjs')).checkCiStatus;
1019
+
1020
+ const ci = await checkCiStatusImpl({ repoRoot, timeoutMs: 15000, sha: head });
1021
+ const gitlab = evaluateCiRow(ci);
1022
+
1023
+ const githubSpec = resolveRepoSpecDep({ repoRoot, vcs: 'github' });
1024
+ let githubCi = null;
1025
+ if (githubSpec) {
1026
+ githubCi = await checkCiStatusImpl({ repoRoot, vcs: 'github', timeoutMs: 15000, sha: head });
1027
+ }
1028
+ const github = evaluateGithubCiRow(githubSpec, githubCi);
1029
+
1030
+ return { gitlab, github };
1031
+ }
1032
+
972
1033
  async function preflight(repoRoot, target, { skipCi = false } = {}) {
973
1034
  const checks = [];
974
1035
  const add = (name, ok, detail = '') => checks.push({ name, ok, detail });
@@ -1098,39 +1159,23 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
1098
1159
  add('npm-token-live', false, err.message);
1099
1160
  }
1100
1161
 
1101
- // 6. CI green on HEAD (the repo's iron session-start rule applies to
1102
- // releases doubly: local green is not evidence — see .claude/rules).
1103
- // --skip-ci is refused under --publish upstream in validateFlags(); it can
1104
- // only reach this branch from --check.
1105
- if (skipCi) {
1106
- add('ci-green-on-head', true, 'SKIPPED via --skip-ci');
1107
- } else {
1108
- const { checkCiStatus } = await import('./lib/ci-status-banner.mjs');
1109
- const ci = await checkCiStatus({ repoRoot, timeoutMs: 15000 });
1110
- const row = evaluateCiRow(ci);
1111
- add('ci-green-on-head', row.ok, row.detail);
1112
- }
1113
-
1114
- // 6b. CI green on the GitHub mirror too. The row above asks the platform
1115
- // `detectVcsFamily` picks for `origin` (GitLab), whose pipeline is Linux-only;
1116
- // the macOS matrix leg lives exclusively in `.github/workflows/test.yml`.
1117
- // `vcs: 'github'` forces the probe onto the mirror without touching the
1118
- // detection order. The GitHub check-runs path reads `commits/HEAD`, i.e. the
1119
- // mirror's default-branch head — which is HEAD only because `head-pushed-github`
1120
- // above proves github/main == local HEAD; that row is this one's precondition,
1121
- // not a duplicate of it.
1122
- if (skipCi) {
1123
- add('ci-green-on-head-github', true, 'SKIPPED via --skip-ci');
1124
- } else {
1125
- const githubSpec = resolveRepoSpec({ repoRoot, vcs: 'github' });
1126
- let githubCi = null;
1127
- if (githubSpec) {
1128
- const { checkCiStatus } = await import('./lib/ci-status-banner.mjs');
1129
- githubCi = await checkCiStatus({ repoRoot, vcs: 'github', timeoutMs: 15000 });
1130
- }
1131
- const row = evaluateGithubCiRow(githubSpec, githubCi);
1132
- add('ci-green-on-head-github', row.ok, row.detail);
1133
- }
1162
+ // 6/6b. CI green on HEAD, both platforms — judged for the commit actually
1163
+ // being released, never whichever HEAD the queried platform reports (see
1164
+ // evaluateCiPreflightRows's docblock for the `sha`-wiring history, #1332).
1165
+ //
1166
+ // GitLab: the repo's iron session-start rule applies to releases doubly —
1167
+ // local green is not evidence — see .claude/rules. GitHub: this second row
1168
+ // exists because `detectVcsFamily` picks `origin` (GitLab) for the row
1169
+ // above, whose pipeline is Linux-only; the macOS matrix leg lives
1170
+ // exclusively in `.github/workflows/test.yml`. `vcs: 'github'` forces the
1171
+ // probe onto the mirror without touching the detection order.
1172
+ // SELF-DISABLING: a checkout with no `github` remote has no mirror to be
1173
+ // red, so `evaluateGithubCiRow` reports `skipped` rather than red — see its
1174
+ // own docblock. --skip-ci is refused under --publish upstream in
1175
+ // validateFlags(); it can only reach this branch from --check.
1176
+ const ciRows = await evaluateCiPreflightRows(repoRoot, head, { skipCi });
1177
+ add('ci-green-on-head', ciRows.gitlab.ok, ciRows.gitlab.detail);
1178
+ add('ci-green-on-head-github', ciRows.github.ok, ciRows.github.detail);
1134
1179
 
1135
1180
  // 7. Leakage gate over the actual pack file list.
1136
1181
  // `npm_config_loglevel` is INHERITED, and `npm pack --dry-run` writes its whole