session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -3,10 +3,16 @@
3
3
  * parse-config.mjs — Parse ## Session Config from CLAUDE.md or AGENTS.md and output validated JSON.
4
4
  * Part of Session Orchestrator v3.0.0 (originally parse-config.sh in v2).
5
5
  *
6
- * Usage: node scripts/parse-config.mjs [path/to/CLAUDE.md|AGENTS.md]
6
+ * Usage: node scripts/parse-config.mjs [--json] [path/to/CLAUDE.md|AGENTS.md]
7
7
  * If no path given, walks up from cwd to find project root and uses its CLAUDE.md (or AGENTS.md).
8
8
  *
9
9
  * Output: Single JSON object to stdout with ALL config fields (defaults applied).
10
+ * `--json` is an explicit NO-OP alias for that default — accepted because
11
+ * `docs/session-config-template.md` (lines 13 and 965) documents
12
+ * `node scripts/parse-config.mjs --json` as THE validation command, and the
13
+ * flag used to be swallowed by the positional-path slot and rejected with
14
+ * `File not found: --json` (exit 1). `cli-design.md` § JSON-First Output also
15
+ * requires every CLI to accept `--json`.
10
16
  * Exit codes: 0 success, 1 error (message to stderr)
11
17
  *
12
18
  * Unparsable lines inside `## Session Config` (#1097) are reported per line on
@@ -25,6 +31,7 @@ import { join, dirname, resolve, parse as parsePath } from 'node:path';
25
31
  import { fileURLToPath } from 'node:url';
26
32
  import { parseSessionConfig } from './lib/config.mjs';
27
33
  import { collectUnparsableLines } from './lib/config/section-extractor.mjs';
34
+ import { findUnterminatedComment } from './lib/config/block-preprocess.mjs';
28
35
  import { ENFORCEMENT_VALUES } from './lib/config-schema.mjs';
29
36
 
30
37
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
@@ -93,10 +100,46 @@ function resolveConfigFile(argPath) {
93
100
  }
94
101
 
95
102
  // ---------------------------------------------------------------------------
96
- // Main
103
+ // Main — argument parsing
97
104
  // ---------------------------------------------------------------------------
105
+ //
106
+ // Exactly two recognised flags plus one optional positional path. `--json` is
107
+ // accepted and IGNORED: stdout is already a single JSON object, and the
108
+ // documented validation command passes the flag. Anything else starting with
109
+ // `-` is rejected rather than silently treated as a filename, so a typo cannot
110
+ // masquerade as a missing file.
111
+
112
+ const HELP_TEXT = `Usage: parse-config.mjs [--json] [path/to/CLAUDE.md|AGENTS.md]
113
+
114
+ Parse the \`## Session Config\` block from CLAUDE.md (or AGENTS.md) and write
115
+ the validated config to stdout as a single JSON object.
116
+
117
+ Options:
118
+ --json No-op alias for the default output (stdout is always JSON).
119
+ --help Show this help and exit 0.
120
+
121
+ Without a path, walks up from cwd to the project root and uses its CLAUDE.md,
122
+ falling back to AGENTS.md. Exit codes: 0 success, 1 error (message on stderr).
123
+ `;
124
+
125
+ let argPath = null;
126
+ for (const arg of process.argv.slice(2)) {
127
+ if (arg === '--help' || arg === '-h') {
128
+ process.stdout.write(HELP_TEXT);
129
+ process.exit(0);
130
+ } else if (arg === '--json') {
131
+ // no-op — the default output already IS JSON
132
+ } else if (arg.startsWith('-')) {
133
+ process.stderr.write(`parse-config.mjs: Unknown option: ${arg}\n\n${HELP_TEXT}`);
134
+ process.exit(1);
135
+ } else if (argPath === null) {
136
+ argPath = arg;
137
+ } else {
138
+ process.stderr.write(`parse-config.mjs: Unexpected extra argument: ${arg}\n\n${HELP_TEXT}`);
139
+ process.exit(1);
140
+ }
141
+ }
98
142
 
99
- const argPath = process.argv[2] ?? null;
100
143
  const configFile = resolveConfigFile(argPath);
101
144
 
102
145
  let content;
@@ -107,6 +150,20 @@ try {
107
150
  process.exit(1);
108
151
  }
109
152
 
153
+ // Unterminated-comment gate — ONE line per session, not one per parser.
154
+ // An `<!--` with no `-->` after it would put the shared HTML-comment skipper in
155
+ // the swallowing state for the rest of the document; `stripHtmlCommentBlocks`
156
+ // now fails CLOSED (returns the lines unfiltered) rather than letting every
157
+ // later block disappear into its defaults, but the operator still has to learn
158
+ // that the document has a defect. This is the surface that says so — never
159
+ // fatal, exit code unchanged.
160
+ const unterminatedCommentLine = findUnterminatedComment(content.split(/\r?\n/));
161
+ if (unterminatedCommentLine !== null) {
162
+ process.stderr.write(
163
+ `⚠ ${parsePath(configFile).base}: unterminated <!-- at line ${unterminatedCommentLine} — comment stripping disabled for the whole document\n`,
164
+ );
165
+ }
166
+
110
167
  let config;
111
168
  try {
112
169
  config = parseSessionConfig(content);
@@ -81,13 +81,14 @@ import {
81
81
  rmSync,
82
82
  realpathSync,
83
83
  } from 'node:fs';
84
- import { join } from 'node:path';
84
+ import { join, relative as relativePath, sep } from 'node:path';
85
85
  import { tmpdir } from 'node:os';
86
86
  import { spawnSync } from 'node:child_process';
87
87
  import { parseArgs } from 'node:util';
88
88
  import { fileURLToPath } from 'node:url';
89
89
 
90
90
  import { resolveRepoSpec } from './lib/vcs-repo-spec.mjs';
91
+ import { enumerateRepoFiles } from './lib/validate/enumerate-repo-files.mjs';
91
92
 
92
93
  const PACKAGE_NAME = 'session-orchestrator';
93
94
  const SPAWN_OPTS = { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 };
@@ -119,6 +120,15 @@ export const SURFACES = [
119
120
  file: '.claude-plugin/plugin.json',
120
121
  patterns: [/"version":\s*"(\d+\.\d+\.\d+)"/],
121
122
  },
123
+ {
124
+ // Root `plugin.json` — the agent-plugins.org 1.0.0 manifest, added in 4.0.0. It is a
125
+ // FOURTH manifest carrying the version, and it was missing from this table on the 4.0.0
126
+ // cut: `--set-version` bumped the other three and `validate-plugin` then failed with
127
+ // "plugin.json version '3.24.0' != package.json version '4.0.0'". Exactly the incident
128
+ // class the table exists for, caught one gate later than it should have been.
129
+ file: 'plugin.json',
130
+ patterns: [/"version":\s*"(\d+\.\d+\.\d+)"/],
131
+ },
122
132
  {
123
133
  file: '.claude-plugin/marketplace.json',
124
134
  patterns: [/"version":\s*"(\d+\.\d+\.\d+)"/g],
@@ -307,7 +317,18 @@ function hasPathSegment(path, segment) {
307
317
 
308
318
  export const LEAKAGE_PATTERNS = [
309
319
  { name: 'tests/', matches: (path) => hasPathSegment(path, 'tests') },
310
- { name: '.orchestrator/', matches: (path) => /\.orchestrator\//.test(path) },
320
+ // `.orchestrator/policy/` is CARVED OUT and everything else under
321
+ // `.orchestrator/` still leaks: the destructive-guard FLOOR policy ships to npm
322
+ // consumers since 4.0.0 (Codex P1 — measured: `npm pack` carried 0 policy
323
+ // entries, so `loadEffectivePolicy` returned `rules:null` and the guard allowed
324
+ // everything on a consumer install). The carve-out is a `policy/` PATH segment,
325
+ // not a prefix: `.orchestrator/metrics/`, `.orchestrator/tmp/`,
326
+ // `.orchestrator/runtime/`, `*.lock` and the `.orchestrator/policy-backup/`
327
+ // prefix trick are all still caught.
328
+ {
329
+ name: '.orchestrator/',
330
+ matches: (path) => /\.orchestrator\//.test(path) && !/^\.orchestrator\/policy\//.test(path),
331
+ },
311
332
  { name: '.claude/', matches: (path) => hasPathSegment(path, '.claude') },
312
333
  { name: '.github/', matches: (path) => /\.github\//.test(path) },
313
334
  { name: 'node_modules', matches: (path) => /node_modules/.test(path) },
@@ -422,15 +443,55 @@ export const MIN_PACKED_ENTRIES = 400;
422
443
  // ---------------------------------------------------------------------------
423
444
 
424
445
  /**
425
- * Drift sweep verdict over a `git grep -l` result.
446
+ * Is EVERY occurrence of `literal` on this line a dependency range (`^X.Y.Z` / `~X.Y.Z`)?
447
+ *
448
+ * One bare occurrence anywhere on the line is enough to call the whole line drift — a comment
449
+ * that also happens to mention a ranged dep must not be excused by that mention.
426
450
  *
427
- * `git grep` exit codes: 0 = matches found, 1 = no match (the success case
428
- * here), anything else = it did not run. Measured on git 2.x: a bad regex and
429
- * a bad pathspec both exit 128; git also documents 2 for usage errors. The old
430
- * inline code read `.stdout` without ever looking at `.status`, so BOTH the
431
- * no-match case and the it-crashed case produced an empty hit list and the
432
- * same reassuring detail line, "no tracked file still carries X". A sweep that
433
- * never ran is not a clean sweep.
451
+ * @param {string} content the matching line's text
452
+ * @param {string} literal the previous release version, matched literally
453
+ * @returns {boolean}
454
+ */
455
+ export function isDependencyRangeOnly(content, literal) {
456
+ let from = 0;
457
+ let seen = 0;
458
+ for (;;) {
459
+ const at = content.indexOf(literal, from);
460
+ if (at === -1) break;
461
+ seen += 1;
462
+ if (!(at > 0 && (content[at - 1] === '^' || content[at - 1] === '~'))) return false;
463
+ from = at + literal.length;
464
+ }
465
+ return seen > 0;
466
+ }
467
+
468
+ /**
469
+ * Drift sweep verdict over a grep-shaped result (`-l` file list or `-n` line hits).
470
+ *
471
+ * Exit-code contract, kept identical to `git grep`'s because that is what this
472
+ * evaluator was written against and what {@link collectDriftHits} now emits:
473
+ * 0 = matches found, 1 = no match (the success case here), anything else = it
474
+ * did not run. Measured on git 2.x: a bad regex and a bad pathspec both exit
475
+ * 128; git also documents 2 for usage errors. The old inline code read
476
+ * `.stdout` without ever looking at `.status`, so BOTH the no-match case and
477
+ * the it-crashed case produced an empty hit list and the same reassuring
478
+ * detail line, "no file still carries X". A sweep that never ran is not a
479
+ * clean sweep.
480
+ *
481
+ * ONE CLASS OF MATCH IS NOT OURS TO BUMP: a dependency RANGE that happens to equal our own
482
+ * previous version. `skills/vault-sync` pins `zod` at `^3.24.0` (the projects-baseline pin), so
483
+ * the 4.0.0 sweep collected two files whose literal belongs to zod and must NOT move when we
484
+ * release. The carve-out is deliberately a PREDICATE and not two allowlist rows: a per-path row
485
+ * fixes this instance and leaves the class open for the next dependency that lands on our version
486
+ * number — the unenumerable-table failure `.claude/rules/measurement-discipline.md` records. A
487
+ * caret- or tilde-prefixed literal is a range, and no version surface of this package is ever
488
+ * written as one (see SURFACES above: every pattern is an exact `"version": "X.Y.Z"`, `vX.Y.Z`
489
+ * or badge form), so the predicate cannot mask a stale surface.
490
+ *
491
+ * Accepts BOTH `git grep` output shapes. A bare `path` (from `-l`) carries no content and is
492
+ * therefore always a hit — the fail-closed reading, unchanged, and the shape {@link collectDriftHits}
493
+ * emits for a file it could not READ. `path:line:content` (from `-n`)
494
+ * is judged per line, and the file counts as drift as soon as ONE matching line is not a range.
434
495
  *
435
496
  * @param {{status: number, stdout?: string, stderr?: string}} grep
436
497
  * @param {string} prevTag — the previous release literal being swept for
@@ -441,21 +502,102 @@ export function evaluateDriftSweep(grep, prevTag, allowlist) {
441
502
  if (grep.status !== 0 && grep.status !== 1) {
442
503
  return {
443
504
  ok: false,
444
- detail: `git grep did not run (exit ${grep.status}): ${(grep.stderr || '').trim().slice(0, 200)} — sweep for ${prevTag} is inconclusive`,
505
+ detail: `drift sweep did not run (exit ${grep.status}): ${(grep.stderr || '').trim().slice(0, 200)} — sweep for ${prevTag} is inconclusive`,
445
506
  };
446
507
  }
447
- const hits = (grep.stdout || '')
448
- .split('\n')
449
- .filter(Boolean)
450
- .filter((f) => !allowlist.test(f));
508
+ const hits = [];
509
+ for (const row of (grep.stdout || '').split('\n').filter(Boolean)) {
510
+ const withContent = row.match(/^(.+?):(\d+):(.*)$/);
511
+ const file = withContent ? withContent[1] : row;
512
+ if (allowlist.test(file)) continue;
513
+ if (withContent && isDependencyRangeOnly(withContent[3], prevTag)) continue;
514
+ if (!hits.includes(file)) hits.push(file);
515
+ }
451
516
  return {
452
517
  ok: hits.length === 0,
453
518
  detail: hits.length
454
519
  ? `still carry ${prevTag}: ${hits.slice(0, 5).join(', ')}`
455
- : `no tracked file outside the allowlist still carries ${prevTag}`,
520
+ : `no file outside the allowlist still carries ${prevTag} (tracked + untracked-not-ignored)`,
456
521
  };
457
522
  }
458
523
 
524
+ /**
525
+ * Bytes of a file inspected when deciding whether it is binary.
526
+ *
527
+ * A NUL byte in the first 8 KB is the same heuristic `git grep`/`grep` use to
528
+ * declare a file binary. NAMED CEILING (BV-004): a text file whose only NUL
529
+ * sits past 8 KB is scanned as text (harmless — it produces no version match),
530
+ * and a binary whose first 8 KB happen to be NUL-free is scanned as text and
531
+ * may emit mojibake rows. Revisit trigger: the first drift-sweep row naming a
532
+ * file nobody recognises as text.
533
+ */
534
+ const BINARY_SNIFF_BYTES = 8192;
535
+
536
+ /**
537
+ * Produce the drift-sweep hit list over the files that EXIST in the working
538
+ * tree — tracked or not — in the exact `{status, stdout, stderr}` shape
539
+ * {@link evaluateDriftSweep} already consumes.
540
+ *
541
+ * ## Why not `git grep` any more (#1248)
542
+ *
543
+ * `git grep` searches the INDEX: it is blind to an untracked file, so the
544
+ * moment a stale version literal is most likely to exist (a doc or manifest
545
+ * written for this release and not yet staged) is exactly the moment the sweep
546
+ * cannot see it and reports clean. The population is now
547
+ * `scripts/lib/validate/enumerate-repo-files.mjs`
548
+ * (`git ls-files --cached --others --exclude-standard`) — tracked PLUS
549
+ * untracked, minus everything `.gitignore` excludes, so `node_modules/` and
550
+ * the peer worktrees under `.claude/worktrees/` stay out without a prune list
551
+ * having to guess at them.
552
+ *
553
+ * What `git grep` bought — searching hidden directories a plain `rg` skips,
554
+ * which is how the forgotten `.codex-plugin` manifest was found — is kept:
555
+ * `git ls-files` lists dotted paths like any other.
556
+ *
557
+ * ## Fail-closed, in two places
558
+ *
559
+ * Enumeration itself throwing is reported as exit 128, which
560
+ * {@link evaluateDriftSweep} reads as "inconclusive" = FAIL — the same reading
561
+ * a crashed `git grep` got. A single file that enumerates but cannot be READ
562
+ * is emitted as a CONTENT-LESS row, which that evaluator already treats as a
563
+ * hit: a file we could not sweep is never silently a clean file.
564
+ *
565
+ * @param {object} options
566
+ * @param {string} options.repoRoot absolute repository root
567
+ * @param {string} options.prevTag the previous release literal to sweep for
568
+ * @param {(o: {repoRoot: string}) => string[]} [options.enumerate] injection seam for tests
569
+ * @param {(absolute: string) => Buffer} [options.read] injection seam for tests
570
+ * @returns {{status: number, stdout: string, stderr: string}}
571
+ */
572
+ export function collectDriftHits({ repoRoot, prevTag, enumerate = enumerateRepoFiles, read = readFileSync }) {
573
+ let files;
574
+ try {
575
+ files = enumerate({ repoRoot });
576
+ } catch (err) {
577
+ return { status: 128, stdout: '', stderr: `enumerateRepoFiles failed: ${err && err.message}` };
578
+ }
579
+ const rows = [];
580
+ for (const absolute of files) {
581
+ const relative = relativePath(repoRoot, absolute).split(sep).join('/');
582
+ let buf;
583
+ try {
584
+ buf = read(absolute);
585
+ } catch {
586
+ // Content-less row = hit (see the evaluator's contract above).
587
+ rows.push(relative);
588
+ continue;
589
+ }
590
+ if (buf.subarray(0, BINARY_SNIFF_BYTES).includes(0)) continue;
591
+ const text = buf.toString('utf8');
592
+ if (!text.includes(prevTag)) continue;
593
+ const lines = text.split('\n');
594
+ for (let i = 0; i < lines.length; i += 1) {
595
+ if (lines[i].includes(prevTag)) rows.push(`${relative}:${i + 1}:${lines[i]}`);
596
+ }
597
+ }
598
+ return { status: rows.length ? 0 : 1, stdout: rows.length ? `${rows.join('\n')}\n` : '', stderr: '' };
599
+ }
600
+
459
601
  /**
460
602
  * Registry-collision verdict over `npm view <pkg> versions --json`.
461
603
  *
@@ -589,6 +731,53 @@ export function evaluateNpmAuth(whoami) {
589
731
  : { ok: false, detail: 'npm whoami exited 0 with an empty identity — the token could not be confirmed' };
590
732
  }
591
733
 
734
+ /**
735
+ * Turn a `checkCiStatus` result into the `ci-green-on-head` preflight row.
736
+ *
737
+ * Three input states since #1031, and the middle one is why this is a named
738
+ * function rather than a ternary: `degraded` means the check could NOT be READ.
739
+ * Interpolating `ci.status` there printed `status: undefined` — a red row whose
740
+ * detail names no cause, which reads as "CI is broken" when the truth is "we
741
+ * never found out". Only an actual `status: 'green'` reading passes; a release
742
+ * must never proceed on an unknown CI state.
743
+ *
744
+ * @param {null | {status?: string, failingJobName?: string, degraded?: string}} ci
745
+ * @returns {{ok: boolean, detail: string}}
746
+ */
747
+ export function evaluateCiRow(ci) {
748
+ if (ci === null || ci === undefined) return { ok: false, detail: 'CI status unavailable' };
749
+ if (ci.degraded) return { ok: false, detail: `CI status unknown (${ci.degraded})` };
750
+ const job = ci.failingJobName ? ` (${ci.failingJobName})` : '';
751
+ return { ok: ci.status === 'green', detail: `status: ${ci.status}${job}` };
752
+ }
753
+
754
+ /**
755
+ * Turn the GitHub-mirror CI reading into the `ci-green-on-head-github` row.
756
+ *
757
+ * WHY A SECOND CI ROW AT ALL: `ci-green-on-head` reads whatever platform
758
+ * `detectVcsFamily` picks for `origin` — GitLab here — and the GitLab pipeline
759
+ * runs Linux only. The **macOS** matrix leg exists solely on the GitHub mirror
760
+ * (`.github/workflows/test.yml`), i.e. on the operator's own platform. A
761
+ * release could therefore go out fully green with the macOS leg red, and the
762
+ * release path would never have asked.
763
+ *
764
+ * SELF-DISABLING, and that is the one place this row is allowed to pass without
765
+ * evidence: a checkout with no `github` remote has no mirror to be red, so the
766
+ * row is `skipped` rather than red. Everything else keeps the three-state
767
+ * contract of `evaluateCiRow` unchanged — `unknown` and `degraded` FAIL, because
768
+ * "we could not read the mirror" is not "the mirror is green" (the fail-closed
769
+ * house rule at the top of this file).
770
+ *
771
+ * @param {string|undefined} repoSpec — `resolveRepoSpec({vcs:'github'})`, undefined when no github remote resolves
772
+ * @param {null | {status?: string, failingJobName?: string, degraded?: string}} ci
773
+ * @returns {{ok: boolean, detail: string}}
774
+ */
775
+ export function evaluateGithubCiRow(repoSpec, ci) {
776
+ if (!repoSpec) return { ok: true, detail: 'skipped — no github remote' };
777
+ const row = evaluateCiRow(ci);
778
+ return { ok: row.ok, detail: `${repoSpec} — ${row.detail}` };
779
+ }
780
+
592
781
  /**
593
782
  * Flag-combination gate, applied before any work.
594
783
  *
@@ -697,12 +886,18 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
697
886
  const changelog = checkChangelogEntry(readFileSync(join(repoRoot, 'CHANGELOG.md'), 'utf8'), target);
698
887
  add('changelog-entry', changelog.ok, changelog.problems.join('; '));
699
888
 
700
- // 3b. Drift sweep: no TRACKED file outside the surfaces table + allowlist
701
- // may still carry the previous release's version literal. `git grep` (not
702
- // rg) on purpose — it searches every tracked file including hidden
703
- // directories, which is exactly how the forgotten .codex-plugin manifest
704
- // was invisible to a plain rg census. Allowlisted: files that legitimately
705
- // carry version HISTORY.
889
+ // 3b. Drift sweep: no file outside the surfaces table + allowlist may still
890
+ // carry the previous release's version literal.
891
+ //
892
+ // POPULATION (#1248, closed here): this used to be one `git grep`, which
893
+ // searches the INDEX and is therefore blind to UNTRACKED files the sweep
894
+ // measured "no TRACKED file still carries X" while its row read as a
895
+ // whole-tree census. It now enumerates via
896
+ // `scripts/lib/validate/enumerate-repo-files.mjs` (tracked PLUS
897
+ // untracked-not-ignored) and greps in-process; see {@link collectDriftHits}
898
+ // for the fail-closed contract and why hidden directories (the forgotten
899
+ // .codex-plugin manifest) are still covered. Allowlisted: files that
900
+ // legitimately carry version HISTORY.
706
901
  const tagList = run('git', ['tag', '-l', 'v*', '--sort=-v:refname'], { cwd: repoRoot });
707
902
  const prevTag = (tagList.stdout || '')
708
903
  .split('\n').map((t) => t.trim().replace(/^v/, ''))
@@ -712,8 +907,11 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
712
907
  // one of them means the sweep is unnecessary.
713
908
  add('drift-sweep', false, `git tag -l failed (exit ${tagList.status}) — cannot determine the previous release to sweep for`);
714
909
  } else if (prevTag) {
715
- const grep = run('git', ['grep', '-l', '--fixed-strings', prevTag, '--', '.'], { cwd: repoRoot });
716
- const sweep = evaluateDriftSweep(grep, prevTag, HISTORY_ALLOWLIST);
910
+ // Line-shaped rows (not a bare file list): the verdict needs the matching LINE, because a
911
+ // caret-ranged dependency that equals our previous version is not drift and a file list
912
+ // cannot show that.
913
+ const hits = collectDriftHits({ repoRoot, prevTag });
914
+ const sweep = evaluateDriftSweep(hits, prevTag, HISTORY_ALLOWLIST);
717
915
  add('drift-sweep', sweep.ok, sweep.detail);
718
916
  } else {
719
917
  add('drift-sweep', true, 'no previous tag to sweep against');
@@ -769,8 +967,29 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
769
967
  } else {
770
968
  const { checkCiStatus } = await import('./lib/ci-status-banner.mjs');
771
969
  const ci = await checkCiStatus({ repoRoot, timeoutMs: 15000 });
772
- const green = ci !== null && ci.status === 'green';
773
- add('ci-green-on-head', green, ci === null ? 'CI status unavailable' : `status: ${ci.status}${ci.failingJobName ? ` (${ci.failingJobName})` : ''}`);
970
+ const row = evaluateCiRow(ci);
971
+ add('ci-green-on-head', row.ok, row.detail);
972
+ }
973
+
974
+ // 6b. CI green on the GitHub mirror too. The row above asks the platform
975
+ // `detectVcsFamily` picks for `origin` (GitLab), whose pipeline is Linux-only;
976
+ // the macOS matrix leg lives exclusively in `.github/workflows/test.yml`.
977
+ // `vcs: 'github'` forces the probe onto the mirror without touching the
978
+ // detection order. The GitHub check-runs path reads `commits/HEAD`, i.e. the
979
+ // mirror's default-branch head — which is HEAD only because `head-pushed-github`
980
+ // above proves github/main == local HEAD; that row is this one's precondition,
981
+ // not a duplicate of it.
982
+ if (skipCi) {
983
+ add('ci-green-on-head-github', true, 'SKIPPED via --skip-ci');
984
+ } else {
985
+ const githubSpec = resolveRepoSpec({ repoRoot, vcs: 'github' });
986
+ let githubCi = null;
987
+ if (githubSpec) {
988
+ const { checkCiStatus } = await import('./lib/ci-status-banner.mjs');
989
+ githubCi = await checkCiStatus({ repoRoot, vcs: 'github', timeoutMs: 15000 });
990
+ }
991
+ const row = evaluateGithubCiRow(githubSpec, githubCi);
992
+ add('ci-green-on-head-github', row.ok, row.detail);
774
993
  }
775
994
 
776
995
  // 7. Leakage gate over the actual pack file list.
@@ -952,6 +1171,37 @@ export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
952
1171
  * @param {{runImpl?: Function, waitImpl?: Function, attempts?: number, delaySeconds?: number}} [deps]
953
1172
  * @returns {{receipt: {confirmed: boolean, target: string, detail: string}, propagation: ReturnType<typeof waitForRegistryPropagation>}}
954
1173
  */
1174
+ /**
1175
+ * The exact `npm publish` invocation: argv plus spawn options.
1176
+ *
1177
+ * WHY THE ENV PIN, AND WHY THIS IS A NAMED FUNCTION: `npm_config_loglevel` is
1178
+ * INHERITED, and the `+ <pkg>@<version>` receipt line — the ONE piece of
1179
+ * evidence `evaluatePublishReceipt` accepts as the irreversible boundary — is
1180
+ * printed at `notice` level. Any ancestor that ran under `npm run --silent`
1181
+ * (the husky pre-push gate does exactly that) therefore hands this child a
1182
+ * silent loglevel, npm publishes successfully and prints nothing, the receipt
1183
+ * reads as unconfirmed, `publish()` throws, and `main()` exits 2 = "pre-receipt,
1184
+ * safe to rerun" while the registry already holds the version. That is the worst
1185
+ * failure this file can produce, and it is the same inherited-silent trap the
1186
+ * leakage gate's `npm pack --dry-run` was already pinned against (see preflight
1187
+ * step 7). The pin makes the receipt independent of the caller's environment.
1188
+ *
1189
+ * Exported because `publish()` itself deliberately is not (it is the
1190
+ * irreversible act) — the invocation it builds is pure, so the pin is testable
1191
+ * without a publishable seam.
1192
+ *
1193
+ * @param {string} repoRoot
1194
+ * @param {string} userconfigPath — the 0600 temp npmrc carrying the token
1195
+ * @returns {{cmd: string, args: string[], opts: {cwd: string, env: object}}}
1196
+ */
1197
+ export function publishInvocation(repoRoot, userconfigPath) {
1198
+ return {
1199
+ cmd: 'npm',
1200
+ args: ['publish', '--access', 'public', '--userconfig', userconfigPath],
1201
+ opts: { cwd: repoRoot, env: { ...process.env, npm_config_loglevel: 'notice' } },
1202
+ };
1203
+ }
1204
+
955
1205
  // Deliberately NOT exported: this is the irreversible act, and every production
956
1206
  // path to it runs through main() -> preflight() (leakage gate, CI gate, dirty-tree
957
1207
  // gate). Exporting it made the whole gate chain bypassable by any importer, and no
@@ -959,9 +1209,10 @@ export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
959
1209
  function publish(repoRoot, target, deps = {}) {
960
1210
  const token = loadNpmToken(repoRoot);
961
1211
  const runImpl = deps.runImpl ?? run;
962
- const res = withTempUserconfig(token, (tmpRc) =>
963
- runImpl('npm', ['publish', '--access', 'public', '--userconfig', tmpRc], { cwd: repoRoot }),
964
- );
1212
+ const res = withTempUserconfig(token, (tmpRc) => {
1213
+ const call = publishInvocation(repoRoot, tmpRc);
1214
+ return runImpl(call.cmd, call.args, call.opts);
1215
+ });
965
1216
  const receipt = evaluatePublishReceipt(res, target);
966
1217
  if (!receipt.confirmed) throw new Error(receipt.detail);
967
1218
 
@@ -1099,6 +1350,46 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
1099
1350
  };
1100
1351
  }
1101
1352
 
1353
+ /**
1354
+ * Render the partial tag/push state left behind by a failed post-receipt tail.
1355
+ *
1356
+ * Pure so the reconciliation lines are unit-testable against a synthetic
1357
+ * outcome: the branch that produces them only exists after an irreversible npm
1358
+ * publish, which no test may perform.
1359
+ *
1360
+ * Absent progress is reported as absent, never as "nothing happened": a
1361
+ * `tagAndPushImpl` that threw before attaching `releaseProgress` leaves state
1362
+ * genuinely unknown, and the operator must inspect rather than assume.
1363
+ *
1364
+ * @param {{tag?: string|null, localTagCreated?: boolean, remotes?: Array<{remote: string, mainPushed?: boolean, tagPushed?: boolean}>}} [tagProgress]
1365
+ * @returns {string[]}
1366
+ */
1367
+ export function describeTagProgress(tagProgress) {
1368
+ const lines = ['\nPARTIAL TAG/PUSH STATE (the npm receipt is already irreversible):'];
1369
+ if (!tagProgress || typeof tagProgress !== 'object') {
1370
+ lines.push(' tag/push progress was not recorded — inspect `git tag -l` and both remotes manually.');
1371
+ return lines;
1372
+ }
1373
+ const tag = tagProgress.tag || 'the release tag';
1374
+ lines.push(
1375
+ tagProgress.localTagCreated === true
1376
+ ? ` local tag ${tag}: CREATED (the next \`--check\` will fail \`tag-free-local\` until it is pushed or deleted).`
1377
+ : ` local tag ${tag}: not created.`,
1378
+ );
1379
+ const remotes = Array.isArray(tagProgress.remotes) ? tagProgress.remotes : [];
1380
+ if (remotes.length === 0) {
1381
+ lines.push(' no remote was reached — neither main nor the tag was pushed anywhere.');
1382
+ return lines;
1383
+ }
1384
+ for (const remote of remotes) {
1385
+ lines.push(
1386
+ ` ${remote.remote}: main ${remote.mainPushed === true ? 'pushed' : 'NOT pushed'}, ` +
1387
+ `tag ${remote.tagPushed === true ? 'pushed' : 'NOT pushed'}.`,
1388
+ );
1389
+ }
1390
+ return lines;
1391
+ }
1392
+
1102
1393
  /**
1103
1394
  * Print a completed publish-tail outcome and return the CLI exit code.
1104
1395
  *
@@ -1120,6 +1411,16 @@ export function printPublishOutcome(outcome, target, io = {}) {
1120
1411
  }
1121
1412
  if (!tagAndPushFailed) {
1122
1413
  log(` tagged ${outcome.tag} (AFTER publish) and pushed main+tag to: ${outcome.pushed.join(', ')}.`);
1414
+ } else {
1415
+ // The npm receipt is already irreversible at this point, so the ONLY thing
1416
+ // that helps the operator is the exact partial state tag-and-push reached.
1417
+ // `runPublishRelease` collects it (`tagProgress.localTagCreated` plus a
1418
+ // per-remote `{mainPushed, tagPushed}`); this printer used to reference
1419
+ // none of it and suppressed the `pushed:` line as well, so the operator was
1420
+ // told only THAT it failed. The local-tag line matters twice over: a
1421
+ // created local tag makes the next `--check` fail `tag-free-local`, which
1422
+ // reads as a mysterious collision unless it was announced here.
1423
+ for (const line of describeTagProgress(outcome.tagProgress)) error(line);
1123
1424
  }
1124
1425
 
1125
1426
  if (outcome.release.skipped) {
@@ -1377,6 +1678,13 @@ async function main() {
1377
1678
  if (!values.publish) return 0;
1378
1679
 
1379
1680
  console.log(`\nPublishing ${PACKAGE_NAME}@${target} ...`);
1681
+ // No spawn timeout is set anywhere in this file, on purpose: a kill in the
1682
+ // middle of `npm publish` or a tag push is the very failure the receipt
1683
+ // boundary exists to avoid. The wall-clock cost is real, though — each of
1684
+ // the two `git push` remotes re-runs the husky pre-push full gate (~2.5 min
1685
+ // each, measured) and the site poll waits up to 120 s.
1686
+ console.log(' This tail can run ~7 minutes (pre-push gate x2 remotes + up to 120s site poll).');
1687
+ console.log(' Run it with a >=600s command timeout or in the background — do NOT kill it mid-run.');
1380
1688
  const outcome = await runPublishRelease(repoRoot, target, { publishImpl: publish });
1381
1689
  return printPublishOutcome(outcome, target);
1382
1690
  }
@@ -40,7 +40,7 @@ import path from 'node:path';
40
40
  import { fileURLToPath } from 'node:url';
41
41
 
42
42
  import { repairLedger, CANONICAL_LEDGER_REL } from './lib/session-record-repair.mjs';
43
- import { SO_PROJECT_DIR } from './lib/platform.mjs';
43
+ import { getProjectDir } from './lib/platform.mjs';
44
44
 
45
45
  const USAGE =
46
46
  'Usage: node scripts/repair-invalid-sessions.mjs [--dry-run|--apply] [--json]\n' +
@@ -52,7 +52,7 @@ const USAGE =
52
52
  CANONICAL_LEDGER_REL +
53
53
  ');\n' +
54
54
  ' must resolve inside <repo-root>/.orchestrator/metrics/\n' +
55
- ' --repo-root project root (default: resolved SO_PROJECT_DIR)\n' +
55
+ ' --repo-root project root (default: resolved project dir)\n' +
56
56
  'Exit codes: 0 completed, 1 arg error, 2 system error, 3 post-verification failed\n';
57
57
 
58
58
  /**
@@ -168,7 +168,7 @@ async function main() {
168
168
 
169
169
  // --apply is an explicit opt-in; absent it (or with --dry-run) we never write.
170
170
  const apply = values.apply === true && values['dry-run'] !== true;
171
- const repoRoot = values['repo-root'] || SO_PROJECT_DIR;
171
+ const repoRoot = values['repo-root'] || getProjectDir();
172
172
  const requestedFile = values.file || path.join(repoRoot, CANONICAL_LEDGER_REL);
173
173
 
174
174
  // MED-3: bound the target to <repoRoot>/.orchestrator/metrics/ BEFORE any I/O.