session-orchestrator 5.11.0 → 5.13.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 (394) hide show
  1. package/.claude-plugin/marketplace.json +12 -1
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +180 -3
  11. package/README.md +126 -117
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +4 -2
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +102 -25
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +55 -21
  47. package/docs/session-config-template.md +32 -24
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +158 -59
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -3
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +73 -3
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/remote-hosts.mjs +2 -2
  143. package/scripts/lib/config/section-extractor.mjs +65 -11
  144. package/scripts/lib/config/template-keys.mjs +153 -0
  145. package/scripts/lib/config/vault-integration.mjs +1 -0
  146. package/scripts/lib/config.mjs +3 -3
  147. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  148. package/scripts/lib/discovery/triage-state.mjs +2 -1
  149. package/scripts/lib/dispatcher/rank.mjs +5 -7
  150. package/scripts/lib/ecosystem-health.mjs +2 -2
  151. package/scripts/lib/eval/engine.mjs +3 -2
  152. package/scripts/lib/eval/session-resolve.mjs +6 -2
  153. package/scripts/lib/eval/sink.mjs +4 -3
  154. package/scripts/lib/events-retention-banner.mjs +3 -2
  155. package/scripts/lib/events-schema.mjs +10 -0
  156. package/scripts/lib/events.mjs +23 -12
  157. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  158. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  159. package/scripts/lib/fleet-protocol.mjs +21 -0
  160. package/scripts/lib/gates/gate-full.mjs +172 -11
  161. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  162. package/scripts/lib/git-config-drift.mjs +141 -7
  163. package/scripts/lib/github-export.mjs +447 -0
  164. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  165. package/scripts/lib/handover-queue-marks.mjs +159 -0
  166. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  167. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  168. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  169. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  170. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  171. package/scripts/lib/host-identity.mjs +9 -5
  172. package/scripts/lib/http-request.mjs +64 -0
  173. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  174. package/scripts/lib/io.mjs +6 -3
  175. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  176. package/scripts/lib/issue-budget.mjs +36 -7
  177. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  178. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  179. package/scripts/lib/learnings/io.mjs +289 -15
  180. package/scripts/lib/learnings/select.mjs +53 -4
  181. package/scripts/lib/learnings/store-role.mjs +209 -0
  182. package/scripts/lib/learnings/surface.mjs +46 -7
  183. package/scripts/lib/ledger-path.mjs +113 -0
  184. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  185. package/scripts/lib/memory-banner.mjs +5 -6
  186. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  187. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  188. package/scripts/lib/memory-proposals/store.mjs +22 -5
  189. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  190. package/scripts/lib/no-follow-open.mjs +57 -0
  191. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  192. package/scripts/lib/own-lock-root.mjs +95 -0
  193. package/scripts/lib/owner-interview.mjs +2 -1
  194. package/scripts/lib/owner-yaml.mjs +70 -7
  195. package/scripts/lib/platform.mjs +161 -9
  196. package/scripts/lib/plugin-update-banner.mjs +15 -11
  197. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  198. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  199. package/scripts/lib/process-group.mjs +270 -34
  200. package/scripts/lib/project-hygiene.mjs +28 -0
  201. package/scripts/lib/quality-gate.mjs +33 -9
  202. package/scripts/lib/reconcile/backlog.mjs +29 -15
  203. package/scripts/lib/reconcile/engine.mjs +26 -2
  204. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  205. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  206. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  207. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  208. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  209. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  210. package/scripts/lib/rule-loader.mjs +20 -10
  211. package/scripts/lib/scope-baseline.mjs +123 -8
  212. package/scripts/lib/scope-echo.mjs +5 -2
  213. package/scripts/lib/scope-gate.mjs +55 -20
  214. package/scripts/lib/session-close-backfill.mjs +75 -7
  215. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  216. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  217. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  218. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  219. package/scripts/lib/session-id.mjs +9 -1
  220. package/scripts/lib/session-identity/own-session.mjs +63 -0
  221. package/scripts/lib/session-invocation.mjs +14 -3
  222. package/scripts/lib/session-lock.mjs +114 -0
  223. package/scripts/lib/session-record-repair.mjs +245 -35
  224. package/scripts/lib/session-registry.mjs +85 -12
  225. package/scripts/lib/session-schema/constants.mjs +9 -0
  226. package/scripts/lib/session-schema/filters.mjs +56 -1
  227. package/scripts/lib/session-schema/validator.mjs +15 -5
  228. package/scripts/lib/session-start-probes.mjs +149 -22
  229. package/scripts/lib/session-token-rollup.mjs +2 -1
  230. package/scripts/lib/session-work-trees.mjs +166 -0
  231. package/scripts/lib/sessions-canonical.mjs +61 -3
  232. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  233. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  234. package/scripts/lib/sh-quote.mjs +1 -0
  235. package/scripts/lib/skill-evidence-window.mjs +5 -1
  236. package/scripts/lib/skill-health/join.mjs +10 -9
  237. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  238. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  239. package/scripts/lib/snapshot-probe.mjs +414 -0
  240. package/scripts/lib/so-hud/collect.mjs +174 -0
  241. package/scripts/lib/so-hud/runtime.mjs +87 -0
  242. package/scripts/lib/spiral-carryover.mjs +8 -1
  243. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  244. package/scripts/lib/state-md/idle-reset.mjs +248 -0
  245. package/scripts/lib/state-md/mission-status.mjs +5 -1
  246. package/scripts/lib/state-md/recommendations.mjs +26 -4
  247. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  248. package/scripts/lib/state-md.mjs +31 -8
  249. package/scripts/lib/sunset/walker.mjs +4 -3
  250. package/scripts/lib/telemetry/consent.mjs +4 -2
  251. package/scripts/lib/telemetry/queue.mjs +3 -3
  252. package/scripts/lib/telemetry/sync.mjs +45 -9
  253. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  254. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  255. package/scripts/lib/ux-grill/paths.mjs +5 -1
  256. package/scripts/lib/validate/check-agents.mjs +37 -6
  257. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  258. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  259. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  260. package/scripts/lib/validate/check-json-files.mjs +3 -1
  261. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  262. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  263. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  264. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  265. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  266. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  267. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  268. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  269. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  270. package/scripts/lib/validate/confidential-names.mjs +3 -1
  271. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  272. package/scripts/lib/vault-mirror/namespace.mjs +22 -9
  273. package/scripts/lib/vault-mirror/process.mjs +197 -65
  274. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  275. package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
  276. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  277. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  278. package/scripts/lib/vault-mirror/utils.mjs +106 -13
  279. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  280. package/scripts/lib/vault-status/board-writer.mjs +34 -5
  281. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  282. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  283. package/scripts/lib/win-process-tree.mjs +137 -0
  284. package/scripts/lib/workspace.mjs +1 -1
  285. package/scripts/lib/worktree/constants.mjs +1 -1
  286. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  287. package/scripts/lib/worktree/listing.mjs +1 -1
  288. package/scripts/lib/zx.mjs +50 -0
  289. package/scripts/mcp-server.mjs +16 -7
  290. package/scripts/mcp-server.sh +13 -83
  291. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  292. package/scripts/memory-propose.mjs +10 -3
  293. package/scripts/migrate-cold-start-seed.mjs +7 -0
  294. package/scripts/migrate-legacy-learnings.sh +22 -0
  295. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  296. package/scripts/migrate-vault-paths.mjs +29 -13
  297. package/scripts/parse-config.mjs +71 -14
  298. package/scripts/print-learnings-index.mjs +25 -2
  299. package/scripts/release-session-lock.mjs +94 -7
  300. package/scripts/release.mjs +451 -24
  301. package/scripts/relocate-vault-corpus.mjs +11 -7
  302. package/scripts/repair-invalid-sessions.mjs +68 -3
  303. package/scripts/resolve-session-invocation.mjs +57 -8
  304. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  305. package/scripts/run-quality-gate.mjs +33 -7
  306. package/scripts/self-update.mjs +56 -7
  307. package/scripts/session-commit-files.mjs +133 -0
  308. package/scripts/site-numbers.mjs +52 -9
  309. package/scripts/snapshot-probe.mjs +156 -0
  310. package/scripts/so-hud.mjs +68 -0
  311. package/scripts/state-md-idle-reset.mjs +82 -0
  312. package/scripts/sweep-expired-learnings.mjs +54 -6
  313. package/scripts/sweep-expired-rules.mjs +4 -1
  314. package/scripts/validate-plugin.mjs +22 -0
  315. package/scripts/vault-integration-watcher.mjs +54 -6
  316. package/scripts/vault-mirror.mjs +9 -0
  317. package/scripts/vault-orphans.mjs +171 -0
  318. package/skills/_shared/fleet-protocol.md +166 -3
  319. package/skills/_shared/instruction-file-resolution.md +1 -1
  320. package/skills/_shared/monitor-patterns.md +44 -25
  321. package/skills/_shared/platform-tools.md +7 -0
  322. package/skills/_shared/state-ownership.md +5 -4
  323. package/skills/architecture/references/domain-model.md +1 -1
  324. package/skills/autopilot/SKILL.md +3 -2
  325. package/skills/bootstrap/SKILL.md +3 -1
  326. package/skills/bootstrap/_shared-template.md +4 -4
  327. package/skills/bootstrap/fast-template.md +1 -1
  328. package/skills/bootstrap/private-contract.md +1 -1
  329. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  330. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  331. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  332. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  333. package/skills/claude-md-drift-check/SKILL.md +3 -3
  334. package/skills/claude-md-drift-check/checker.mjs +61 -101
  335. package/skills/close/SKILL.md +20 -7
  336. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  337. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  338. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  339. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  340. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  341. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  342. package/skills/discovery/probes-session.md +3 -0
  343. package/skills/discovery/probes-vault.md +2 -0
  344. package/skills/dispatcher/SKILL.md +1 -1
  345. package/skills/eval/SKILL.md +3 -1
  346. package/skills/evolve/SKILL.md +2 -0
  347. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  348. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  349. package/skills/gitlab-ops/SKILL.md +7 -6
  350. package/skills/gitlab-portfolio/SKILL.md +2 -2
  351. package/skills/go/SKILL.md +13 -1
  352. package/skills/npm-publish/SKILL.md +5 -2
  353. package/skills/plan/SKILL.md +23 -10
  354. package/skills/plan/mode-feature.md +13 -8
  355. package/skills/plan/mode-new.md +6 -14
  356. package/skills/plan/prd-feature-template.md +19 -0
  357. package/skills/plan/prd-full-template.md +12 -5
  358. package/skills/plan/prd-premortem-panel.md +48 -0
  359. package/skills/plan/prd-reviewer-prompt.md +11 -3
  360. package/skills/plan/soul.md +1 -1
  361. package/skills/portfolio/SKILL.md +5 -6
  362. package/skills/quality-gates/SKILL.md +2 -4
  363. package/skills/reconcile/SKILL.md +27 -4
  364. package/skills/release/SKILL.md +16 -8
  365. package/skills/session-end/SKILL.md +97 -13
  366. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  367. package/skills/session-end/phase-3-6-tail.md +11 -7
  368. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  369. package/skills/session-end/plan-verification.md +16 -1
  370. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  371. package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
  372. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  373. package/skills/session-end/references/session-summary-template.md +8 -0
  374. package/skills/session-end/session-metrics-write.md +7 -4
  375. package/skills/session-plan/SKILL.md +5 -3
  376. package/skills/session-start/SKILL.md +10 -4
  377. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  378. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  379. package/skills/session-start/phase-8-5-express-path.md +4 -3
  380. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  381. package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
  382. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  383. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  384. package/skills/tmux-layout/SKILL.md +2 -2
  385. package/skills/vault-sync/validator.mjs +3 -2
  386. package/skills/wave-executor/SKILL.md +5 -4
  387. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  388. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  389. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  390. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  391. package/AGENTS.md +0 -207
  392. package/assets/og-card.png +0 -0
  393. package/assets/og-card.svg +0 -47
  394. package/assets/wave-lifecycle.svg +0 -98
@@ -21,8 +21,12 @@
21
21
  // they are enforced by --check instead.
22
22
  // --check Preflight: surface parity, CHANGELOG entry present
23
23
  // + Unreleased folded, drift sweep, tag collision
24
- // (local, origin, github), github/main mirror parity,
25
- // npm registry collision, npm token liveness, CI green
24
+ // (local, origin, github), github/main mirror parity
25
+ // (under `mirror: curated`: github/main's Source-Commit
26
+ // trailer == HEAD instead of the sha, plus a green
27
+ // snapshot-probe receipt for HEAD's export tree),
28
+ // npm registry collision, npm token liveness and
29
+ // publish capability (Bypass-2FA), CI green
26
30
  // on HEAD, leakage gate over retained archive filenames and contents.
27
31
  // --publish Runs --check first, then token publish via temp
28
32
  // userconfig (NPM_TOKEN from .env.local). A
@@ -30,7 +34,10 @@
30
34
  // boundary: before it, failure aborts normally; after it,
31
35
  // never rerun --publish. The tail tags AFTER receipt
32
36
  // (never before — eliminates "tagged but unpublished"),
33
- // pushes main + tag to origin AND github, handles the
37
+ // pushes main + tag to origin AND github (under
38
+ // `mirror: curated`: the verified export snapshot to
39
+ // github main and the tag on THAT snapshot — never HEAD
40
+ // or main, #1645), handles the
34
41
  // GitHub release (`--verify-tag`), reconciles registry
35
42
  // propagation, and polls the live site. A tag/push
36
43
  // failure skips the tag-dependent GitHub-release and
@@ -102,6 +109,16 @@ import { fileURLToPath } from 'node:url';
102
109
  import { resolveRepoSpec } from './lib/vcs-repo-spec.mjs';
103
110
  import { enumerateRepoFiles } from './lib/validate/enumerate-repo-files.mjs';
104
111
  import { writeJsonAtomicSync } from './lib/io.mjs';
112
+ import {
113
+ buildGithubExport,
114
+ createGitRunner,
115
+ parseSourceCommitTrailer,
116
+ plainPushGuard,
117
+ pushUrlProblem,
118
+ readMirrorSetting,
119
+ verifyExportCommit,
120
+ } from './lib/github-export.mjs';
121
+ import { computeSnapshotTree, readSnapshotProbeReceipt } from './lib/snapshot-probe.mjs';
105
122
 
106
123
  const PACKAGE_NAME = 'session-orchestrator';
107
124
  const SPAWN_OPTS = { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 };
@@ -958,6 +975,172 @@ export function evaluateRemoteHeadParity(remote, ls, head, branch = 'main') {
958
975
  : { ok: false, detail: `${remote}/${branch} at ${sha.slice(0, 8)}, HEAD at ${head.slice(0, 8)} — the mirror is behind` };
959
976
  }
960
977
 
978
+ /**
979
+ * `head-pushed-github` under `mirror: curated` (#1645). The public main is a
980
+ * snapshot commit, never HEAD, so sha parity can never hold; what must hold is
981
+ * that github/main IS the snapshot of the commit being released — its
982
+ * `Source-Commit` trailer equals HEAD. Fail-closed on every gathering failure:
983
+ * an unreadable ls-remote, no sha, or a commit object that is not local.
984
+ *
985
+ * @param {{status: number, stdout?: string, stderr?: string}} ls — `git ls-remote github refs/heads/main`
986
+ * @param {string} head — the local HEAD sha
987
+ * @param {string|null} commitBody — `git cat-file commit <github main>` output, null when unreadable
988
+ * @returns {{ok: boolean, detail: string, sha: string|null}}
989
+ */
990
+ export function evaluateCuratedMirrorParity(ls, head, commitBody) {
991
+ if (ls.status !== 0) {
992
+ return { ok: false, sha: null, detail: `ls-remote github failed (exit ${ls.status}): ${(ls.stderr || '').trim().slice(0, 200)}` };
993
+ }
994
+ const sha = (ls.stdout || '').trim().split(/\s+/)[0] || '';
995
+ if (!/^[0-9a-f]{40}$/i.test(sha)) {
996
+ return { ok: false, sha: null, detail: 'ls-remote github returned no sha for refs/heads/main — cannot compare' };
997
+ }
998
+ if (commitBody === null) {
999
+ return { ok: false, sha, detail: `github/main ${sha.slice(0, 8)} is not available locally (git fetch github failed) — snapshot source unknown` };
1000
+ }
1001
+ const source = parseSourceCommitTrailer(commitBody);
1002
+ if (!source) {
1003
+ return { ok: false, sha, detail: `github/main ${sha.slice(0, 8)} carries no Source-Commit trailer — publish the curated snapshot first (/close, mirror: curated)` };
1004
+ }
1005
+ return source === head
1006
+ ? { ok: true, sha, detail: `curated: github/main ${sha.slice(0, 8)} is the snapshot of HEAD ${head.slice(0, 8)}` }
1007
+ : { ok: false, sha, detail: `curated: github/main ${sha.slice(0, 8)} is the snapshot of ${source.slice(0, 8)}, HEAD at ${head.slice(0, 8)} — the mirror is behind (/close publishes it)` };
1008
+ }
1009
+
1010
+ /**
1011
+ * The two GitHub-mirror preflight rows plus the sha GitHub's CI must be asked
1012
+ * about. Reads `mirror` exactly as session-end § 4.4 does (`readMirrorSetting`):
1013
+ * - `curated`: `head-pushed-github` is the trailer parity above, and the CI sha
1014
+ * is the snapshot (GitHub never sees HEAD itself).
1015
+ * - anything else: the plain sha parity, unchanged — but the plain push is only
1016
+ * allowed when `plainPushGuard` agrees (no export policy in HEAD or on
1017
+ * origin's main), so a checkout whose Session Config predates
1018
+ * `mirror: curated` fails `github-mirror-mode` instead of later pushing the
1019
+ * private history.
1020
+ * - `none`: GitHub is not a release target at all — `skip` is true and the
1021
+ * github rows are reported as skipped (release.mjs used to push anyway).
1022
+ *
1023
+ * - every mode but `none`: `pushUrl` is the `github-push-url` row (#1693). The
1024
+ * publish tail refuses a push URL that differs from the fetch URL
1025
+ * (`pushUrlProblem`), but only AFTER the irreversible npm publish; the
1026
+ * preflight must see the same refusal first. A checkout with no `github`
1027
+ * remote has no push target, so the row is `skipped`. The detail is
1028
+ * `pushUrlProblem`'s own message, which names the remote but never a URL
1029
+ * (a URL may carry credentials).
1030
+ *
1031
+ * @param {string} repoRoot
1032
+ * @param {string} head
1033
+ * @returns {{ skip: boolean, curated?: boolean, mode: {ok: boolean, detail: string}, parity: {ok: boolean, detail: string}, pushUrl: {ok: boolean, detail: string}, ciSha: string }}
1034
+ */
1035
+ export function evaluateGithubMirrorPreflight(repoRoot, head) {
1036
+ const setting = readMirrorSetting(repoRoot);
1037
+ if (setting.ok && setting.mirror === 'none') {
1038
+ const skipped = { ok: true, detail: "skipped — 'mirror: none'" };
1039
+ return { skip: true, mode: skipped, parity: skipped, pushUrl: skipped, ciSha: head };
1040
+ }
1041
+ const pushUrl = evaluateGithubPushUrl(repoRoot);
1042
+ const ls = run('git', ['ls-remote', 'github', 'refs/heads/main'], { cwd: repoRoot });
1043
+ if (setting.ok && setting.mirror === 'curated') {
1044
+ const sha = ls.status === 0 ? (ls.stdout || '').trim().split(/\s+/)[0] : '';
1045
+ let body = null;
1046
+ if (/^[0-9a-f]{40}$/i.test(sha)) {
1047
+ const readBody = () => run('git', ['cat-file', 'commit', sha], { cwd: repoRoot });
1048
+ let cat = readBody();
1049
+ if (cat.status !== 0) {
1050
+ run('git', ['fetch', '--quiet', '--no-tags', 'github', 'refs/heads/main'], { cwd: repoRoot });
1051
+ cat = readBody();
1052
+ }
1053
+ body = cat.status === 0 ? cat.stdout : null;
1054
+ }
1055
+ const parity = evaluateCuratedMirrorParity(ls, head, body);
1056
+ return {
1057
+ skip: false,
1058
+ curated: true,
1059
+ mode: { ok: true, detail: 'curated — GitHub receives the export snapshot, never HEAD' },
1060
+ parity: { ok: parity.ok, detail: parity.detail },
1061
+ pushUrl,
1062
+ ciSha: parity.ok && parity.sha ? parity.sha : head,
1063
+ };
1064
+ }
1065
+ const parity = evaluateRemoteHeadParity('github', ls, head);
1066
+ if (!setting.ok) return { skip: false, mode: { ok: false, detail: setting.reason }, parity, pushUrl, ciSha: head };
1067
+ const verdict = plainPushGuard({ git: createGitRunner(repoRoot), explicitGithub: setting.mirror === 'github' });
1068
+ return {
1069
+ skip: false,
1070
+ mode: verdict.ok
1071
+ ? { ok: true, detail: `plain (mirror: ${setting.mirror ?? 'unset'})` }
1072
+ : { ok: false, detail: `plain push refused — ${verdict.reason}` },
1073
+ parity,
1074
+ pushUrl,
1075
+ ciSha: head,
1076
+ };
1077
+ }
1078
+
1079
+ /** Steps that run the test suite; a receipt without one proves only validate-plugin. */
1080
+ const SNAPSHOT_SUITE_STEPS = ['test', 'remote-gate'];
1081
+ const SNAPSHOT_PROBE_REMEDY = 'run: node scripts/snapshot-probe.mjs --host <alias> (or --local)';
1082
+
1083
+ /**
1084
+ * Verdict of the `snapshot-probe` preflight row (#1712). Release 5.13.0 found
1085
+ * three blockers only after the first curated GitHub push, because nobody had
1086
+ * ever run the suite on the exported tree. Green only for a receipt of THIS
1087
+ * snapshot tree with `ok: true`, every step `pass`, and at least one suite step
1088
+ * (`test` or `remote-gate`) — a validate-plugin-only receipt never ran a test.
1089
+ * Runs nothing itself. Every red detail ends with the remediation.
1090
+ *
1091
+ * @param {{ tree: {ok: boolean, snapshotTree?: string, reason?: string}, receiptResult?: {ok: boolean, receipt: any, reason?: string}|null }} input
1092
+ * @returns {{ ok: boolean, detail: string }}
1093
+ */
1094
+ export function evaluateSnapshotProbeRow({ tree, receiptResult }) {
1095
+ const red = (why) => ({ ok: false, detail: `${why} — ${SNAPSHOT_PROBE_REMEDY}` });
1096
+ if (!tree?.ok) return red(`snapshot tree not computable (${tree?.reason ?? 'unknown'})`);
1097
+ const short = String(tree.snapshotTree).slice(0, 8);
1098
+ if (!receiptResult?.ok) {
1099
+ const reason = receiptResult?.reason ?? 'absent';
1100
+ return red(reason === 'absent' ? `no probe receipt for snapshot tree ${short}` : `probe receipt for tree ${short} ${reason}`);
1101
+ }
1102
+ const receipt = receiptResult.receipt;
1103
+ const steps = Array.isArray(receipt?.steps) ? receipt.steps : [];
1104
+ const failed = steps.filter((s) => s?.status !== 'pass').map((s) => `${s?.name ?? '?'}=${s?.status ?? '?'}`);
1105
+ if (failed.length > 0) return red(`probe of tree ${short} red: ${failed.join(', ')}`);
1106
+ if (receipt.ok !== true) return red(`probe receipt for tree ${short} is not ok`);
1107
+ const names = steps.map((s) => s.name);
1108
+ if (!names.some((n) => SNAPSHOT_SUITE_STEPS.includes(n))) {
1109
+ return red(`probe of tree ${short} ran no suite step (${names.join(', ') || 'no steps'})`);
1110
+ }
1111
+ return { ok: true, detail: `tree ${short} probed green (${names.join(', ')}; route ${receipt.route ?? 'unknown'})` };
1112
+ }
1113
+
1114
+ /**
1115
+ * Wiring of the `snapshot-probe` row: only under `mirror: curated` (read off the
1116
+ * {@link evaluateGithubMirrorPreflight} verdict). Under any other mode GitHub
1117
+ * never receives an exported tree, so the row is skipped.
1118
+ * @param {string} repoRoot @param {string} head @param {{ curated?: boolean }} mirror
1119
+ * @returns {{ ok: boolean, detail: string }}
1120
+ */
1121
+ export function evaluateSnapshotProbePreflight(repoRoot, head, mirror) {
1122
+ if (!mirror?.curated) return { ok: true, detail: 'skipped — not mirror: curated' };
1123
+ const tree = computeSnapshotTree({ repoRoot, source: head });
1124
+ const receiptResult = tree.ok ? readSnapshotProbeReceipt({ repoRoot, snapshotTree: tree.snapshotTree }) : null;
1125
+ return evaluateSnapshotProbeRow({ tree, receiptResult });
1126
+ }
1127
+
1128
+ /**
1129
+ * The `github-push-url` preflight row (#1693): red when a push to `github`
1130
+ * would reach a different URL than the one every github read goes through.
1131
+ * @param {string} repoRoot
1132
+ * @returns {{ok: boolean, detail: string}}
1133
+ */
1134
+ function evaluateGithubPushUrl(repoRoot) {
1135
+ const remotes = run('git', ['remote'], { cwd: repoRoot });
1136
+ if (remotes.status !== 0) return { ok: false, detail: `git remote failed (exit ${remotes.status}) — push URL unknown` };
1137
+ if (!(remotes.stdout || '').split('\n').map((r) => r.trim()).includes('github')) {
1138
+ return { ok: true, detail: "skipped — no 'github' remote" };
1139
+ }
1140
+ const problem = pushUrlProblem(createGitRunner(repoRoot), 'github');
1141
+ return problem ? { ok: false, detail: problem } : { ok: true, detail: 'push URL equals fetch URL' };
1142
+ }
1143
+
961
1144
  /**
962
1145
  * npm-auth verdict over `npm whoami --userconfig <tmp>`.
963
1146
  *
@@ -981,6 +1164,78 @@ export function evaluateNpmAuth(whoami) {
981
1164
  : { ok: false, detail: 'npm whoami exited 0 with an empty identity — the token could not be confirmed' };
982
1165
  }
983
1166
 
1167
+ /**
1168
+ * Does `abbreviated` (an `npm token list --json` `token` field) denote
1169
+ * `fullToken`? npm 11 prints the first 8 characters, a literal `...`, and the
1170
+ * last 4 (measured 2026-10-21, npm 11.19.1). Any other shape is no match —
1171
+ * guessing a looser form would let a different token answer for this one.
1172
+ *
1173
+ * @param {string} fullToken
1174
+ * @param {unknown} abbreviated
1175
+ * @returns {boolean}
1176
+ */
1177
+ export function matchAbbreviatedNpmToken(fullToken, abbreviated) {
1178
+ if (typeof fullToken !== 'string' || typeof abbreviated !== 'string') return false;
1179
+ const m = abbreviated.match(/^(.{8})\.\.\.(.{4})$/);
1180
+ if (!m) return false;
1181
+ return fullToken.length > 12 && fullToken.startsWith(m[1]) && fullToken.endsWith(m[2]);
1182
+ }
1183
+
1184
+ /**
1185
+ * Judge whether the token in use can publish NON-INTERACTIVELY (#1712).
1186
+ *
1187
+ * `npm-token-live` (`npm whoami`) proves only that the token authenticates.
1188
+ * Release 5.13.0 passed it and then failed `npm publish` with EOTP: the
1189
+ * granular token lacked "Bypass 2FA". Without bypass, publish fails with EOTP
1190
+ * (account with 2FA) or E403 (account without) — either way the release
1191
+ * cannot finish, so `bypass_2fa !== true` is red, never a warning.
1192
+ *
1193
+ * The caller matches the token and strips `token`/`key`/`id` before calling;
1194
+ * this function never sees a token value, and its details name only the
1195
+ * token's NAME. Fail-closed: anything it cannot confirm is red.
1196
+ *
1197
+ * @param {{matches?: number, entry?: {name?: string, bypass_2fa?: unknown,
1198
+ * permissions?: Array<{name?: string, action?: string}>,
1199
+ * scopes?: Array<{name?: string|null, type?: string}>, revoked?: unknown,
1200
+ * expiry?: string|null} | null, now?: Date, packageName?: string, error?: string}} input
1201
+ * @returns {{ok: boolean, detail: string}}
1202
+ */
1203
+ export function evaluateNpmPublishCapability({ matches, entry, now = new Date(), packageName = PACKAGE_NAME, error } = {}) {
1204
+ if (error) return { ok: false, detail: `npm token list failed: ${String(error).slice(0, 200)}` };
1205
+ if (matches !== 1 || !entry) {
1206
+ return { ok: false, detail: `cannot identify the token in use in npm token list (matches: ${matches})` };
1207
+ }
1208
+ const name = String(entry.name ?? '(unnamed)').slice(0, 80);
1209
+ if (entry.revoked !== null && entry.revoked !== undefined) {
1210
+ return { ok: false, detail: `token '${name}' is revoked` };
1211
+ }
1212
+ let expiry = null;
1213
+ if (entry.expiry !== null && entry.expiry !== undefined) {
1214
+ expiry = new Date(entry.expiry);
1215
+ if (Number.isNaN(expiry.getTime())) return { ok: false, detail: `token '${name}' has an unreadable expiry` };
1216
+ if (expiry.getTime() <= now.getTime()) {
1217
+ return { ok: false, detail: `token '${name}' expired ${expiry.toISOString().slice(0, 10)}` };
1218
+ }
1219
+ }
1220
+ const perms = Array.isArray(entry.permissions) ? entry.permissions : [];
1221
+ if (!perms.some((p) => p?.name === 'package' && p?.action === 'write')) {
1222
+ return { ok: false, detail: `token '${name}' has no package write permission` };
1223
+ }
1224
+ // A package scope with `name: null` covers all packages (measured shape).
1225
+ const scopes = Array.isArray(entry.scopes) ? entry.scopes : [];
1226
+ if (!scopes.some((sc) => sc?.type === 'package' && (sc.name === null || sc.name === packageName))) {
1227
+ return { ok: false, detail: `token '${name}' is not scoped to ${packageName}` };
1228
+ }
1229
+ if (entry.bypass_2fa !== true) {
1230
+ return {
1231
+ ok: false,
1232
+ detail: `token '${name}' lacks Bypass-2FA — npm publish will fail with EOTP/E403; create a granular token per skills/npm-publish § Token requirements`,
1233
+ };
1234
+ }
1235
+ const expires = expiry ? `expires ${expiry.toISOString().slice(0, 10)}` : 'no expiry';
1236
+ return { ok: true, detail: `token '${name}': bypass-2FA, write, ${expires}` };
1237
+ }
1238
+
984
1239
  /**
985
1240
  * Turn a `checkCiStatus` result into the `ci-green-on-head` preflight row.
986
1241
  *
@@ -1006,8 +1261,8 @@ export function evaluateNpmAuth(whoami) {
1006
1261
  * those two. With neither present the text is byte-identical to
1007
1262
  * before, so a plain green or a named failing job reads exactly as it did.
1008
1263
  *
1009
- * @param {null | {status?: string, failingJobName?: string, degraded?: string,
1010
- * detail?: string, details?: {reason?: string}}} ci
1264
+ * @param {null | {status?: string, failingJobName?: string, failingJobNames?: string[],
1265
+ * degraded?: string, detail?: string, details?: {reason?: string}}} ci
1011
1266
  * @returns {{ok: boolean, detail: string}}
1012
1267
  */
1013
1268
  export function evaluateCiRow(ci) {
@@ -1018,7 +1273,11 @@ export function evaluateCiRow(ci) {
1018
1273
  }
1019
1274
  // A failing job NAMES the failure, so it wins over the generic reason;
1020
1275
  // `reason` is what fills the gap when there is no job to name (`unknown`).
1021
- const reason = ci.failingJobName || ci.details?.reason || '';
1276
+ // Several red jobs are ALL named (#1712): 5.13.0 showed only the first, and
1277
+ // a red informational Windows run sat beside the real cause. The names come
1278
+ // from `failingJobNames`, sanitised at generation like `failingJobName`.
1279
+ const names = Array.isArray(ci.failingJobNames) ? ci.failingJobNames : [];
1280
+ const reason = (names.length > 1 ? names.join(', ') : ci.failingJobName) || ci.details?.reason || '';
1022
1281
  const why = reason ? ` (${reason})` : '';
1023
1282
  return { ok: ci.status === 'green', detail: `status: ${ci.status}${why}` };
1024
1283
  }
@@ -1149,6 +1408,8 @@ function readPackageVersion(repoRoot) {
1149
1408
  * local HEAD `preflight()` already computed for the remote-parity rows).
1150
1409
  * @param {{
1151
1410
  * skipCi?: boolean,
1411
+ * githubSha?: string,
1412
+ * skipGithub?: boolean,
1152
1413
  * checkCiStatus?: Function,
1153
1414
  * resolveRepoSpec?: (opts: { repoRoot: string, vcs: 'github' }) => string|undefined,
1154
1415
  * }} [deps]
@@ -1156,6 +1417,11 @@ function readPackageVersion(repoRoot) {
1156
1417
  */
1157
1418
  export async function evaluateCiPreflightRows(repoRoot, head, {
1158
1419
  skipCi = false,
1420
+ // Under `mirror: curated` GitHub never sees `head`; its CI ran on the snapshot
1421
+ // whose Source-Commit is `head` (#1645). Plain mirrors keep `head`.
1422
+ githubSha = head,
1423
+ // `mirror: none`: GitHub is no release target, so its CI is not consulted.
1424
+ skipGithub = false,
1159
1425
  checkCiStatus: checkCiStatusDep,
1160
1426
  resolveRepoSpec: resolveRepoSpecDep = resolveRepoSpec,
1161
1427
  } = {}) {
@@ -1168,10 +1434,11 @@ export async function evaluateCiPreflightRows(repoRoot, head, {
1168
1434
  const ci = await checkCiStatusImpl({ repoRoot, timeoutMs: 15000, sha: head });
1169
1435
  const gitlab = evaluateCiRow(ci);
1170
1436
 
1437
+ if (skipGithub) return { gitlab, github: { ok: true, detail: "skipped — 'mirror: none'" } };
1171
1438
  const githubSpec = resolveRepoSpecDep({ repoRoot, vcs: 'github' });
1172
1439
  let githubCi = null;
1173
1440
  if (githubSpec) {
1174
- githubCi = await checkCiStatusImpl({ repoRoot, vcs: 'github', timeoutMs: 15000, sha: head });
1441
+ githubCi = await checkCiStatusImpl({ repoRoot, vcs: 'github', timeoutMs: 15000, sha: githubSha });
1175
1442
  }
1176
1443
  const github = evaluateGithubCiRow(githubSpec, githubCi);
1177
1444
 
@@ -1219,11 +1486,18 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
1219
1486
  : '',
1220
1487
  );
1221
1488
  const head = run('git', ['rev-parse', 'HEAD'], { cwd: repoRoot }).stdout.trim();
1222
- for (const remote of ['origin', 'github']) {
1223
- const ls = run('git', ['ls-remote', remote, 'refs/heads/main'], { cwd: repoRoot });
1224
- const parity = evaluateRemoteHeadParity(remote, ls, head);
1225
- add(`head-pushed-${remote}`, parity.ok, parity.detail);
1226
- }
1489
+ const originLs = run('git', ['ls-remote', 'origin', 'refs/heads/main'], { cwd: repoRoot });
1490
+ const originParity = evaluateRemoteHeadParity('origin', originLs, head);
1491
+ add('head-pushed-origin', originParity.ok, originParity.detail);
1492
+ // github: sha parity for a plain mirror, Source-Commit parity under
1493
+ // `mirror: curated` (#1645) — see evaluateGithubMirrorPreflight.
1494
+ const mirror = evaluateGithubMirrorPreflight(repoRoot, head);
1495
+ add('github-mirror-mode', mirror.mode.ok, mirror.mode.detail);
1496
+ add('head-pushed-github', mirror.parity.ok, mirror.parity.detail);
1497
+ add('github-push-url', mirror.pushUrl.ok, mirror.pushUrl.detail);
1498
+ // Curated only: the exported tree must have been probed (#1712).
1499
+ const probe = evaluateSnapshotProbePreflight(repoRoot, head, mirror);
1500
+ add('snapshot-probe', probe.ok, probe.detail);
1227
1501
 
1228
1502
  // 2. Surface parity.
1229
1503
  const surfaceRows = scanSurfaces(repoRoot, target);
@@ -1277,7 +1551,7 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
1277
1551
  ? `git tag -l failed (exit ${localTagRes.status}) — local tag collision unknown`
1278
1552
  : localTag && `${tag} already exists locally`,
1279
1553
  );
1280
- for (const remote of ['origin', 'github']) {
1554
+ for (const remote of mirror.skip ? ['origin'] : ['origin', 'github']) {
1281
1555
  const ls = run('git', ['ls-remote', '--tags', remote, `refs/tags/${tag}`], { cwd: repoRoot });
1282
1556
  const collision = ls.status === 0 && ls.stdout.trim() !== '';
1283
1557
  add(`tag-free-${remote}`, ls.status === 0 && !collision, collision ? `${tag} already on ${remote}` : ls.status !== 0 ? `ls-remote ${remote} failed` : '');
@@ -1307,6 +1581,35 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
1307
1581
  add('npm-token-live', false, err.message);
1308
1582
  }
1309
1583
 
1584
+ // 5c. npm token PUBLISH capability (#1712). `whoami` was green for 5.13.0,
1585
+ // yet publish failed with EOTP: the token lacked Bypass-2FA. `npm token list`
1586
+ // (works with a granular token; `npm profile get` does not) carries that
1587
+ // flag. The token is matched INSIDE this block and only the stripped entry
1588
+ // leaves it — no token value, prefix or key reaches a detail string.
1589
+ try {
1590
+ const token = loadNpmToken(repoRoot);
1591
+ const listed = withTempUserconfig(token, (rc) => {
1592
+ const res = run('npm', ['token', 'list', '--json', '--userconfig', rc], { cwd: repoRoot });
1593
+ if (res.status !== 0) return { error: `exit ${res.status}: ${(res.stderr || '').trim().slice(0, 200)}` };
1594
+ let list;
1595
+ try {
1596
+ list = JSON.parse(res.stdout || '');
1597
+ } catch {
1598
+ return { error: 'output is not JSON' };
1599
+ }
1600
+ if (!Array.isArray(list)) return { error: 'output is not a JSON array' };
1601
+ const hits = list.filter((e) => matchAbbreviatedNpmToken(token, e?.token));
1602
+ if (hits.length !== 1) return { matches: hits.length, entry: null };
1603
+ // Destructured only to strip them: they never leave this closure.
1604
+ const { token: _abbr, key: _key, id: _id, ...entry } = hits[0];
1605
+ return { matches: 1, entry };
1606
+ });
1607
+ const verdict = evaluateNpmPublishCapability(listed);
1608
+ add('npm-token-publish', verdict.ok, verdict.detail);
1609
+ } catch (err) {
1610
+ add('npm-token-publish', false, err.message);
1611
+ }
1612
+
1310
1613
  // 6/6b. CI green on HEAD, both platforms — judged for the commit actually
1311
1614
  // being released, never whichever HEAD the queried platform reports (see
1312
1615
  // evaluateCiPreflightRows's docblock for the `sha`-wiring history, #1332).
@@ -1321,11 +1624,13 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
1321
1624
  // red, so `evaluateGithubCiRow` reports `skipped` rather than red — see its
1322
1625
  // own docblock. --skip-ci is refused under --publish upstream in
1323
1626
  // validateFlags(); it can only reach this branch from --check.
1324
- const ciRows = await evaluateCiPreflightRows(repoRoot, head, { skipCi });
1627
+ if (mirror.skip) add('tag-free-github', true, "skipped — 'mirror: none'");
1628
+ const ciRows = await evaluateCiPreflightRows(repoRoot, head, { skipCi, githubSha: mirror.ciSha, skipGithub: mirror.skip });
1325
1629
  add('ci-green-on-head', ciRows.gitlab.ok, ciRows.gitlab.detail);
1326
1630
  add('ci-green-on-head-github', ciRows.github.ok, ciRows.github.detail);
1327
1631
 
1328
- return checks;
1632
+ // `head` is handed to the publish tail, which refuses to tag anything else.
1633
+ return { checks, head };
1329
1634
  }
1330
1635
 
1331
1636
  function changelogExcerpt(repoRoot, target) {
@@ -1401,7 +1706,11 @@ export function evaluatePublishReceipt(result, target) {
1401
1706
  target,
1402
1707
  detail: confirmed
1403
1708
  ? `${PACKAGE_NAME}@${target} receipt confirmed`
1404
- : `npm publish did not emit a target-confirmed receipt for ${PACKAGE_NAME}@${target} (exit ${result.status})`,
1709
+ : /\bEOTP\b/.test(output)
1710
+ // #1712: npm asks for a one-time password BEFORE uploading, so nothing
1711
+ // reached the registry and a re-run is safe.
1712
+ ? `npm publish failed with EOTP (exit ${result.status}): the token lacks Bypass-2FA — nothing was published; re-running --publish with a Bypass-2FA granular token is safe (skills/npm-publish § Token requirements)`
1713
+ : `npm publish did not emit a target-confirmed receipt for ${PACKAGE_NAME}@${target} (exit ${result.status})`,
1405
1714
  };
1406
1715
  }
1407
1716
 
@@ -1835,10 +2144,100 @@ export function settlePublishReceipt(repoRoot, target, res, deps = {}) {
1835
2144
  return { receipt, propagation, proof };
1836
2145
  }
1837
2146
 
1838
- function tagAndPush(repoRoot, target) {
2147
+ /**
2148
+ * Put the curated snapshot of `source` on github main and return its sha
2149
+ * (`mirror: curated`, #1645). Mirrors session-end § 4.4: the export is built on
2150
+ * the ls-remote-verified github main, re-verified BY SHA right before the push,
2151
+ * and that sha — never a ref name, never HEAD — is pushed. When github main
2152
+ * already is the snapshot of `source` (the normal case: `/close` published it
2153
+ * and the preflight proved the trailer), nothing is pushed and that sha is
2154
+ * returned; an unchanged tree whose trailer names another commit refuses,
2155
+ * because the tag must point at the snapshot OF the release commit.
2156
+ *
2157
+ * @param {string} repoRoot
2158
+ * @param {string} source — full sha of the release commit
2159
+ * @param {{ snapshotCheck?: Function }} [deps]
2160
+ * @returns {string}
2161
+ */
2162
+ function publishCuratedSnapshot(repoRoot, source, deps = {}) {
2163
+ // The export requires refs/remotes/github/main == the remote's main.
2164
+ mustRun('git', ['fetch', '--quiet', '--no-tags', 'github', '+refs/heads/main:refs/remotes/github/main'], { cwd: repoRoot });
2165
+ const git = createGitRunner(repoRoot);
2166
+ const built = buildGithubExport({ git, source, ...(deps.snapshotCheck ? { snapshotCheck: deps.snapshotCheck } : {}) });
2167
+ if (built.status === 'created') {
2168
+ const verdict = verifyExportCommit({ git, sha: built.sha, source });
2169
+ if (!verdict.ok) throw new Error(`curated snapshot ${built.sha} failed verification — ${verdict.reason}`);
2170
+ mustRun('git', ['push', 'github', `${built.sha}:refs/heads/main`], { cwd: repoRoot });
2171
+ return built.sha;
2172
+ }
2173
+ if (built.status === 'unchanged') {
2174
+ const body = git(['cat-file', 'commit', built.sha]);
2175
+ if (body.status !== 0 || parseSourceCommitTrailer(body.stdout) !== source) {
2176
+ throw new Error(`github main ${built.sha} has the release tree but is not the snapshot of ${source} — refusing to tag it`);
2177
+ }
2178
+ const pushProblem = pushUrlProblem(git, 'github');
2179
+ if (pushProblem) throw new Error(pushProblem);
2180
+ return built.sha;
2181
+ }
2182
+ throw new Error(`curated export refused — ${built.reason}`);
2183
+ }
2184
+
2185
+ /**
2186
+ * Why the publish tail may still tag HEAD, or null when it may (#1645). The
2187
+ * preflight checked one commit; minutes later (npm publish in between) HEAD can
2188
+ * have moved — a parallel session switching branch is enough — and whatever
2189
+ * HEAD is then gets tagged and, under `mirror: curated`, snapshotted onto
2190
+ * GitHub main. So, right before `git tag`: HEAD must still be the preflight
2191
+ * HEAD, on branch main, and equal origin's main read live (the same source
2192
+ * anchor session-end § 4.4 uses for curated pushes).
2193
+ *
2194
+ * @param {string} repoRoot
2195
+ * @param {string|undefined} expectedHead — the HEAD the preflight checked
2196
+ * @returns {string|null}
2197
+ */
2198
+ export function releaseAnchorProblem(repoRoot, expectedHead) {
2199
+ if (typeof expectedHead !== 'string' || !/^[0-9a-f]{40}$/.test(expectedHead)) {
2200
+ return 'no preflight HEAD handed in — the release commit is not anchored';
2201
+ }
2202
+ const head = run('git', ['rev-parse', 'HEAD'], { cwd: repoRoot });
2203
+ const now = head.status === 0 ? head.stdout.trim() : '';
2204
+ if (now !== expectedHead) return `HEAD moved since preflight (${expectedHead.slice(0, 8)} → ${now.slice(0, 8) || 'unreadable'})`;
2205
+ const branch = run('git', ['branch', '--show-current'], { cwd: repoRoot });
2206
+ const name = branch.status === 0 ? branch.stdout.trim() : '';
2207
+ if (name !== 'main') return `HEAD is on '${name || 'detached'}', not main`;
2208
+ const origin = evaluateRemoteHeadParity('origin', run('git', ['ls-remote', 'origin', 'refs/heads/main'], { cwd: repoRoot }), expectedHead);
2209
+ return origin.ok ? null : `HEAD is not origin's main — ${origin.detail}`;
2210
+ }
2211
+
2212
+ /**
2213
+ * Tag the release commit and push main + tag to origin and the GitHub mirror.
2214
+ *
2215
+ * Under `mirror: curated` (read like session-end § 4.4) github never receives
2216
+ * main or the release commit: it gets the export snapshot as main, and the tag
2217
+ * `v<target>` is pushed onto THAT snapshot (#1645, decided: same tag name,
2218
+ * different target sha — the GitLab tag is the annotated tag on the release
2219
+ * commit, the GitHub tag a lightweight tag on its public snapshot). A plain
2220
+ * mirror is unchanged, except that `plainPushGuard` must allow it. Under
2221
+ * `mirror: none` GitHub is skipped entirely (`githubSkipped: true`). Nothing is
2222
+ * tagged unless {@link releaseAnchorProblem} accepts HEAD.
2223
+ *
2224
+ * @param {string} repoRoot
2225
+ * @param {string} target
2226
+ * @param {{ expectedHead?: string, snapshotCheck?: Function }} [deps] — `expectedHead`: the
2227
+ * preflight HEAD (required); `snapshotCheck`: test seam (the default leakage scan needs a
2228
+ * host owner policy)
2229
+ * @returns {{ tag: string, pushed: string[], githubSnapshot?: string, githubSkipped?: boolean }}
2230
+ */
2231
+ export function tagAndPush(repoRoot, target, deps = {}) {
1839
2232
  const tag = `v${target}`;
1840
2233
  const progress = { tag, localTagCreated: false, pushed: [], remotes: [] };
1841
2234
  try {
2235
+ const setting = readMirrorSetting(repoRoot);
2236
+ if (!setting.ok) throw new Error(`GitHub mirror mode unknown — ${setting.reason}; nothing tagged or pushed`);
2237
+ const curated = setting.mirror === 'curated';
2238
+ const githubSkipped = setting.mirror === 'none';
2239
+ const anchor = releaseAnchorProblem(repoRoot, deps.expectedHead);
2240
+ if (anchor) throw new Error(`release anchor lost — ${anchor}; nothing tagged or pushed`);
1842
2241
  const excerpt = changelogExcerpt(repoRoot, target);
1843
2242
  const msgDir = mkdtempSync(join(tmpdir(), 'release-tagmsg-'));
1844
2243
  const msgFile = join(msgDir, 'msg');
@@ -1849,16 +2248,36 @@ function tagAndPush(repoRoot, target) {
1849
2248
  } finally {
1850
2249
  rmSync(msgDir, { recursive: true, force: true });
1851
2250
  }
1852
- for (const remote of ['origin', 'github']) {
2251
+ for (const remote of githubSkipped ? ['origin'] : ['origin', 'github']) {
1853
2252
  const remoteProgress = { remote, mainPushed: false, tagPushed: false };
1854
2253
  progress.remotes.push(remoteProgress);
2254
+ if (remote === 'github' && curated) {
2255
+ const released = mustRun('git', ['rev-parse', `${tag}^{commit}`], { cwd: repoRoot }).stdout.trim();
2256
+ if (released !== deps.expectedHead) throw new Error(`${tag} points at ${released}, not the preflight HEAD — refusing to publish it to github`);
2257
+ const snapshot = publishCuratedSnapshot(repoRoot, released, deps);
2258
+ remoteProgress.mainPushed = true;
2259
+ progress.githubSnapshot = snapshot;
2260
+ mustRun('git', ['push', 'github', `${snapshot}:refs/tags/${tag}`], { cwd: repoRoot });
2261
+ remoteProgress.tagPushed = true;
2262
+ progress.pushed.push(remote);
2263
+ continue;
2264
+ }
2265
+ if (remote === 'github') {
2266
+ const verdict = plainPushGuard({ git: createGitRunner(repoRoot), explicitGithub: setting.mirror === 'github' });
2267
+ if (!verdict.ok) throw new Error(`plain push to github refused — ${verdict.reason}`);
2268
+ }
1855
2269
  mustRun('git', ['push', remote, 'main'], { cwd: repoRoot });
1856
2270
  remoteProgress.mainPushed = true;
1857
2271
  mustRun('git', ['push', remote, tag], { cwd: repoRoot });
1858
2272
  remoteProgress.tagPushed = true;
1859
2273
  progress.pushed.push(remote);
1860
2274
  }
1861
- return { tag, pushed: progress.pushed };
2275
+ return {
2276
+ tag,
2277
+ pushed: progress.pushed,
2278
+ ...(progress.githubSnapshot ? { githubSnapshot: progress.githubSnapshot } : {}),
2279
+ ...(githubSkipped ? { githubSkipped: true } : {}),
2280
+ };
1862
2281
  } catch (err) {
1863
2282
  const failure = err instanceof Error ? err : new Error(String(err));
1864
2283
  failure.releaseProgress = progress;
@@ -1873,7 +2292,8 @@ function tagAndPush(repoRoot, target) {
1873
2292
  *
1874
2293
  * @param {string} repoRoot
1875
2294
  * @param {string} target
1876
- * @param {{publishImpl?: Function, tagAndPushImpl?: Function, ensureGithubReleaseImpl?: Function, verifyLiveSiteImpl?: Function}} [deps]
2295
+ * @param {{publishImpl?: Function, tagAndPushImpl?: Function, ensureGithubReleaseImpl?: Function, verifyLiveSiteImpl?: Function, expectedHead?: string}} [deps]
2296
+ * `expectedHead` — the HEAD the preflight checked; the tail refuses to tag any other commit.
1877
2297
  * @returns {Promise<{status: 'complete'|'post-publish-reconciliation', receipt: object, tag: string|null, pushed: string[], tagProgress?: object, release: object, live: object, propagation: object, reconciliation: Array<{phase: string, kind?: string, detail: string}>}>}
1878
2298
  */
1879
2299
  export async function runPublishRelease(repoRoot, target, deps = {}) {
@@ -1898,7 +2318,7 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
1898
2318
  const propagation = publication.propagation;
1899
2319
  let tagAndPushResult;
1900
2320
  try {
1901
- tagAndPushResult = tagAndPushImpl(repoRoot, target);
2321
+ tagAndPushResult = tagAndPushImpl(repoRoot, target, { expectedHead: deps.expectedHead });
1902
2322
  } catch (err) {
1903
2323
  const rawProgress = err?.releaseProgress;
1904
2324
  const remotes = Array.isArray(rawProgress?.remotes)
@@ -1935,8 +2355,10 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
1935
2355
  };
1936
2356
  }
1937
2357
 
1938
- const { tag, pushed } = tagAndPushResult;
1939
- const release = ensureGithubReleaseImpl(repoRoot, target);
2358
+ const { tag, pushed, githubSnapshot, githubSkipped } = tagAndPushResult;
2359
+ const release = githubSkipped
2360
+ ? { ok: true, skipped: true, state: 'skipped-mirror-none', detail: "GitHub release skipped — 'mirror: none'" }
2361
+ : ensureGithubReleaseImpl(repoRoot, target);
1940
2362
  const live = await verifyLiveSiteImpl(target);
1941
2363
  const reconciliation = [];
1942
2364
  if (!propagation?.ok) {
@@ -1954,6 +2376,7 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
1954
2376
  receipt: publication.receipt,
1955
2377
  tag,
1956
2378
  pushed,
2379
+ ...(githubSnapshot ? { githubSnapshot } : {}),
1957
2380
  release,
1958
2381
  live,
1959
2382
  propagation,
@@ -2030,6 +2453,9 @@ export function printPublishOutcome(outcome, target, io = {}) {
2030
2453
  }
2031
2454
  if (!tagAndPushFailed) {
2032
2455
  log(` tagged ${outcome.tag} (AFTER publish) and pushed main+tag to: ${outcome.pushed.join(', ')}.`);
2456
+ if (outcome.githubSnapshot) {
2457
+ log(` github (mirror: curated): main and ${outcome.tag} are the public snapshot ${outcome.githubSnapshot.slice(0, 12)}, not the release commit.`);
2458
+ }
2033
2459
  } else {
2034
2460
  // The npm receipt is already irreversible at this point, so the ONLY thing
2035
2461
  // that helps the operator is the exact partial state tag-and-push reached.
@@ -2329,7 +2755,7 @@ async function main() {
2329
2755
 
2330
2756
  if (values.check || values.publish) {
2331
2757
  const target = readPackageVersion(repoRoot);
2332
- const checks = await preflight(repoRoot, target, { skipCi: values['skip-ci'] });
2758
+ const { checks, head } = await preflight(repoRoot, target, { skipCi: values['skip-ci'] });
2333
2759
  const checked = await withCheckedPackage(repoRoot, async (artifact) => {
2334
2760
  checks.push({ name: 'leakage-gate', ok: true, detail: 'packed filenames and required owner-policy content scan verified' });
2335
2761
  const ok = printChecks(checks, values.json && !values.publish, target);
@@ -2339,6 +2765,7 @@ async function main() {
2339
2765
  console.log(' Publish and post-receipt reconciliation may take several minutes; do not kill it mid-run.');
2340
2766
  const outcome = await runPublishRelease(repoRoot, target, {
2341
2767
  publishImpl: (root, version) => publish(root, version, { artifact }),
2768
+ expectedHead: head,
2342
2769
  });
2343
2770
  return printPublishOutcome(outcome, target);
2344
2771
  });