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
@@ -0,0 +1,447 @@
1
+ /**
2
+ * github-export.mjs — curated public GitHub mirror as ONE snapshot commit (#1631, #1594).
3
+ *
4
+ * GitLab keeps the full private repository. The public GitHub mirror (`mirror: curated`)
5
+ * receives, per export, a single SQUASH commit whose tree is the source tree minus the
6
+ * paths in `.orchestrator/policy/public-export.json`, and whose parent is ALWAYS the
7
+ * public GitHub main — verified against the remote itself (`git ls-remote`), never
8
+ * trusted from the local tracking ref alone. Nothing but that one new commit sits
9
+ * between the public main and the export, so nothing private can ride along: no older
10
+ * snapshot built under an older policy, no forged or stale export ref, no private
11
+ * commit from the source history.
12
+ *
13
+ * Plumbing only, on a temp index — the real index, working tree and HEAD are untouched:
14
+ * GIT_INDEX_FILE=<tmp> read-tree <source>^{tree}
15
+ * → update-index --force-remove (paths matched by the SAME matcher the leakage
16
+ * checker uses, so the two can never disagree on what "excluded" means)
17
+ * → write-tree → commit-tree -p <verified github main> (trailer `Source-Commit: <sha>`)
18
+ * → update-ref refs/heads/github-export <new> <old-or-zero> (compare-and-swap)
19
+ *
20
+ * `refs/heads/github-export` is only the output slot: overwritten each run, never read
21
+ * as a parent. Every git call runs with GIT_NO_REPLACE_OBJECTS=1, and the export refuses
22
+ * outright when `refs/replace/*` or `info/grafts` exist — either would let the checks read
23
+ * a different object graph than the one `git push` sends.
24
+ *
25
+ * The git runner is injectable so tests and callers can wrap it; the snapshot check is
26
+ * injectable so plumbing tests do not depend on a host owner-pattern policy. The default
27
+ * snapshot check runs the strict leakage scan and refuses when a public file still cites
28
+ * an excluded path, so the curated tree cannot turn GitHub's validate-plugin red.
29
+ */
30
+
31
+ import { spawnSync } from 'node:child_process';
32
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
33
+ import { tmpdir } from 'node:os';
34
+ import { dirname, join, relative, resolve } from 'node:path';
35
+ import { fileURLToPath } from 'node:url';
36
+
37
+ import {
38
+ PUBLIC_EXPORT_POLICY_FILE,
39
+ isPublicExportExcluded,
40
+ validatePublicExportPolicy,
41
+ } from './validate/check-owner-leakage.mjs';
42
+ import { scanSkillScriptPaths } from './validate/check-skill-script-paths.mjs';
43
+ import { checkSkillLinks } from './validate/check-skill-links.mjs';
44
+
45
+ export const EXPORT_REF = 'refs/heads/github-export';
46
+ export const MIRROR_REMOTE = 'github';
47
+ export const MIRROR_REF = 'refs/remotes/github/main';
48
+ export const SOURCE_COMMIT_TRAILER = 'Source-Commit';
49
+
50
+ const LEAKAGE_SCRIPT = fileURLToPath(new URL('./validate/check-owner-leakage.mjs', import.meta.url));
51
+ const PARSE_CONFIG_SCRIPT = fileURLToPath(new URL('../parse-config.mjs', import.meta.url));
52
+ const MIRROR_VALUES = ['none', 'github', 'curated'];
53
+
54
+ /**
55
+ * @typedef {{ status: number|null, stdout: string, stderr: string }} GitResult
56
+ * @typedef {(args: string[], opts?: { env?: Record<string,string|undefined>, input?: string }) => GitResult} GitRunner
57
+ */
58
+
59
+ /**
60
+ * Default git runner: never throws, returns status + output.
61
+ * @param {string} cwd
62
+ * @returns {GitRunner}
63
+ */
64
+ export function createGitRunner(cwd) {
65
+ return (args, { env, input } = {}) => {
66
+ const res = spawnSync('git', args, {
67
+ cwd,
68
+ // Replace refs would let every check below read a different graph than push sends.
69
+ env: { ...process.env, ...(env ?? {}), GIT_NO_REPLACE_OBJECTS: '1' },
70
+ input,
71
+ encoding: 'utf8',
72
+ maxBuffer: 256 * 1024 * 1024,
73
+ });
74
+ return { status: res.status, stdout: res.stdout ?? '', stderr: res.stderr ?? (res.error ? String(res.error.message) : '') };
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Last `Source-Commit:` trailer value of a raw commit object (`git cat-file commit`).
80
+ * @param {string} commitBody
81
+ * @returns {string|null} full hex sha or null
82
+ */
83
+ export function parseSourceCommitTrailer(commitBody) {
84
+ const re = new RegExp(`^${SOURCE_COMMIT_TRAILER}: ([0-9a-f]{40,64})\\s*$`, 'gm');
85
+ let last = null;
86
+ for (const m of commitBody.matchAll(re)) last = m[1];
87
+ return last;
88
+ }
89
+
90
+ /** @param {GitRunner} git @param {string} rev */
91
+ function resolveCommit(git, rev) {
92
+ const res = git(['rev-parse', '--verify', '--quiet', `${rev}^{commit}`]);
93
+ return res.status === 0 ? res.stdout.trim() : null;
94
+ }
95
+
96
+ /** @param {GitRunner} git */
97
+ function isAncestor(git, ancestor, descendant) {
98
+ return git(['merge-base', '--is-ancestor', ancestor, descendant]).status === 0;
99
+ }
100
+
101
+ /** @param {GitRunner} git @param {string} treeish */
102
+ function listTree(git, treeish) {
103
+ const res = git(['ls-tree', '-r', '-z', '--name-only', treeish]);
104
+ if (res.status !== 0) throw new Error(`git ls-tree failed for ${treeish}`);
105
+ return res.stdout.split('\0').filter(Boolean);
106
+ }
107
+
108
+ /**
109
+ * Public files that still cite an excluded path, judged by the SAME two validate-plugin
110
+ * modules the GitHub CI runs (`check-skill-script-paths` for inline-code citations,
111
+ * `check-skill-links` for relative links), executed on the snapshot tree. Only findings
112
+ * that point INTO an excluded path count: those are the ones the export itself creates,
113
+ * so a source tree that passes validate-plugin cannot produce a snapshot that fails it.
114
+ * A tool error in the path scan counts as a finding (fail closed).
115
+ * @param {string} root snapshot work tree
116
+ * @param {string[]} excludes
117
+ * @returns {string[]} `file:line → cited` entries
118
+ */
119
+ export function findExcludedCitations(root, excludes) {
120
+ const hit = (rel) => isPublicExportExcluded(rel, excludes) || isPublicExportExcluded(`${rel}/`, excludes);
121
+ const out = [];
122
+ const paths = scanSkillScriptPaths({ pluginRoot: root });
123
+ if (paths.toolError) out.push('path-check tool error — citations not measurable');
124
+ for (const f of paths.findings) {
125
+ if (f.severity !== 'warn' && hit(f.path)) out.push(`${f.file}:${f.line} → ${f.path}`);
126
+ }
127
+ for (const f of checkSkillLinks(root).findings) {
128
+ const target = relative(root, resolve(dirname(join(root, f.file)), f.target.split('#')[0])).split('\\').join('/');
129
+ if (hit(target)) out.push(`${f.file}:${f.line} → ${f.target}`);
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /**
135
+ * Default snapshot check: materialise the snapshot commit in a temporary detached
136
+ * worktree, run `check-owner-leakage.mjs --public-export` over it, then
137
+ * {@link findExcludedCitations}. The worktree is always removed. Only the leakage
138
+ * checker's own (already redacted) FAIL lines are surfaced; citation findings name
139
+ * public files and excluded paths, which the public policy file already lists.
140
+ * @param {{ git: GitRunner, commit: string, excludes: string[] }} ctx
141
+ * @returns {{ ok: boolean, reason?: string, findings?: string[] }}
142
+ */
143
+ export function defaultSnapshotCheck({ git, commit, excludes }) {
144
+ const dir = mkdtempSync(join(tmpdir(), 'so-github-export-'));
145
+ const wt = join(dir, 'tree');
146
+ let added = false;
147
+ try {
148
+ const add = git(['-c', 'core.hooksPath=/dev/null', 'worktree', 'add', '--detach', '--quiet', wt, commit]);
149
+ if (add.status !== 0) return { ok: false, reason: 'temporary worktree for the leakage scan could not be created' };
150
+ added = true;
151
+ const res = spawnSync(process.execPath, [LEAKAGE_SCRIPT, wt, '--public-export'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
152
+ const findings = (res.stdout ?? '').split('\n').filter((l) => l.startsWith(' FAIL:'));
153
+ if (res.status !== 0) return { ok: false, reason: `leakage scan failed (${findings.length} finding(s))`, findings };
154
+ const citations = findExcludedCitations(wt, excludes);
155
+ if (citations.length > 0) {
156
+ return { ok: false, reason: `${citations.length} public file citation(s) point into excluded paths (validate-plugin would fail on GitHub)`, findings: citations };
157
+ }
158
+ return { ok: true };
159
+ } finally {
160
+ if (added) git(['worktree', 'remove', '--force', wt]);
161
+ rmSync(dir, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ /** Replace refs or grafts make every read disagree with what `git push` sends. @param {GitRunner} git */
166
+ function graphIntegrityProblem(git) {
167
+ const replaceRefs = git(['for-each-ref', '--format=%(refname)', 'refs/replace/']);
168
+ if (replaceRefs.status !== 0 || replaceRefs.stdout.trim() !== '') return 'refs/replace/* present — remove replace refs before exporting';
169
+ const grafts = git(['rev-parse', '--path-format=absolute', '--git-path', 'info/grafts']);
170
+ if (grafts.status !== 0 || existsSync(grafts.stdout.trim())) return 'info/grafts present — remove grafts before exporting';
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Exclude list from the SOURCE commit's policy file.
176
+ * @param {GitRunner} git @param {string} sourceSha
177
+ * @returns {{ ok: true, excludes: string[] } | { ok: false, reason: string }}
178
+ */
179
+ export function readSourcePolicy(git, sourceSha) {
180
+ const res = git(['show', `${sourceSha}:${PUBLIC_EXPORT_POLICY_FILE}`]);
181
+ if (res.status !== 0) return { ok: false, reason: `${PUBLIC_EXPORT_POLICY_FILE} missing in source commit` };
182
+ try {
183
+ return validatePublicExportPolicy(JSON.parse(res.stdout));
184
+ } catch {
185
+ return { ok: false, reason: `${PUBLIC_EXPORT_POLICY_FILE} is not JSON` };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Write the curated snapshot TREE of a source commit: the source tree minus every path
191
+ * the policy excludes, via a temp index (`read-tree` → `update-index --force-remove` →
192
+ * `write-tree`). Writes loose tree objects only — no commit, no ref; the real index is
193
+ * untouched. The written tree is post-checked independently of the removal step.
194
+ * Shared by `buildGithubExport` and the snapshot probe (#1712), so the tree the probe
195
+ * tests is by construction the tree the export publishes.
196
+ * @param {{ git: GitRunner, sourceSha: string, excludes: string[] }} opts
197
+ * @returns {{ ok: true, tree: string, sourceFiles: number, removed: string[] } | { ok: false, reason: string }}
198
+ */
199
+ export function writeSnapshotTree({ git, sourceSha, excludes }) {
200
+ const fail = (reason) => ({ ok: /** @type {const} */ (false), reason });
201
+ const tmp = mkdtempSync(join(tmpdir(), 'so-github-export-idx-'));
202
+ const env = { GIT_INDEX_FILE: join(tmp, 'index') };
203
+ let tree;
204
+ let sourceFiles;
205
+ let removed;
206
+ try {
207
+ if (git(['read-tree', `${sourceSha}^{tree}`], { env }).status !== 0) return fail('read-tree of the source failed');
208
+ const ls = git(['ls-files', '-z', '--cached'], { env });
209
+ if (ls.status !== 0) return fail('listing the temp index failed');
210
+ const all = ls.stdout.split('\0').filter(Boolean);
211
+ sourceFiles = all.length;
212
+ removed = all.filter((p) => isPublicExportExcluded(p, excludes));
213
+ if (removed.length > 0) {
214
+ const rm = git(['update-index', '--force-remove', '-z', '--stdin'], { env, input: removed.join('\0') + '\0' });
215
+ if (rm.status !== 0) return fail('removing excluded paths from the temp index failed');
216
+ }
217
+ const wt = git(['write-tree'], { env });
218
+ if (wt.status !== 0) return fail('write-tree failed');
219
+ tree = wt.stdout.trim();
220
+ } finally {
221
+ rmSync(tmp, { recursive: true, force: true });
222
+ }
223
+
224
+ // Post-check on the written tree itself, independent of the removal step above.
225
+ const survivors = listTree(git, tree).filter((p) => isPublicExportExcluded(p, excludes));
226
+ if (survivors.length > 0) return fail(`${survivors.length} excluded path(s) survived in the snapshot tree`);
227
+ return { ok: true, tree, sourceFiles, removed };
228
+ }
229
+
230
+ /**
231
+ * The anchor (`ls-remote`) is read through the FETCH URL, but `git push` goes to the
232
+ * push URL (`remote.<name>.pushurl`, `url.*.pushInsteadOf`). If they differ, the
233
+ * verified main describes a different repository than the one that receives the push.
234
+ * URLs are compared, never printed.
235
+ * @param {GitRunner} git @param {string} remote
236
+ * @returns {string|null} problem, or null when both resolve to the same URL
237
+ */
238
+ export function pushUrlProblem(git, remote) {
239
+ const fetchUrl = git(['remote', 'get-url', remote]);
240
+ const pushUrl = git(['remote', 'get-url', '--push', remote]);
241
+ if (fetchUrl.status !== 0 || pushUrl.status !== 0) return `remote '${remote}' URLs not readable`;
242
+ if (fetchUrl.stdout.trim() !== pushUrl.stdout.trim()) {
243
+ return `the push URL of '${remote}' differs from its fetch URL (pushurl or pushInsteadOf) — the verified main would not describe the push target`;
244
+ }
245
+ return null;
246
+ }
247
+
248
+ /**
249
+ * Public GitHub main: the tracking ref, required to equal the remote's real main.
250
+ * @param {GitRunner} git @param {string} remote @param {string} mirrorRef
251
+ * @returns {{ ok: true, sha: string } | { ok: false, reason: string }}
252
+ */
253
+ function verifiedPublicMain(git, remote, mirrorRef) {
254
+ const mirrorSha = resolveCommit(git, mirrorRef);
255
+ if (!mirrorSha) return { ok: false, reason: `${mirrorRef} does not resolve — fetch ${remote} first` };
256
+ const ls = git(['ls-remote', remote, 'refs/heads/main']);
257
+ const remoteSha = ls.status === 0 ? (ls.stdout.split('\n').find((l) => l.endsWith('\trefs/heads/main')) ?? '').split('\t')[0] : '';
258
+ if (!remoteSha) return { ok: false, reason: `git ls-remote ${remote} refs/heads/main failed — public main not verifiable` };
259
+ if (remoteSha !== mirrorSha) return { ok: false, reason: `${mirrorRef} does not match ${remote}'s main — fetch ${remote} first` };
260
+ return { ok: true, sha: mirrorSha };
261
+ }
262
+
263
+ /**
264
+ * Re-verify a snapshot commit BY SHA right before it is pushed (session-end § 4.4 runs
265
+ * this through `github-export.mjs --verify <sha> --source <sha>`). Binding the check to
266
+ * the sha — and pushing that sha, not a ref name — closes the window in which the export
267
+ * ref could be swapped after the scan. Objects are immutable by sha once replace refs
268
+ * and grafts are ruled out.
269
+ * @param {{ git: GitRunner, sha: string, source: string, remote?: string, mirrorRef?: string }} opts
270
+ * @returns {{ ok: true } | { ok: false, reason: string }}
271
+ */
272
+ export function verifyExportCommit({ git, sha, source, remote = MIRROR_REMOTE, mirrorRef = MIRROR_REF }) {
273
+ const bad = (reason) => ({ ok: /** @type {const} */ (false), reason });
274
+ const integrity = graphIntegrityProblem(git);
275
+ if (integrity) return bad(integrity);
276
+ const commit = resolveCommit(git, sha);
277
+ const sourceSha = resolveCommit(git, source);
278
+ if (!commit || commit !== sha) return bad('the export sha is not a full commit sha');
279
+ if (!sourceSha) return bad(`source '${source}' does not resolve to a commit`);
280
+ const pushProblem = pushUrlProblem(git, remote);
281
+ if (pushProblem) return bad(pushProblem);
282
+ const main = verifiedPublicMain(git, remote, mirrorRef);
283
+ if (!main.ok) return bad(main.reason);
284
+ const parents = git(['rev-list', '--parents', '-n', '1', commit]).stdout.trim().split(' ').slice(1);
285
+ if (parents.length !== 1 || parents[0] !== main.sha) return bad('the export commit is not one commit directly on the public main');
286
+ const body = git(['cat-file', 'commit', commit]);
287
+ if (body.status !== 0 || parseSourceCommitTrailer(body.stdout) !== sourceSha) return bad(`the export commit's ${SOURCE_COMMIT_TRAILER} is not the source`);
288
+ const policy = readSourcePolicy(git, sourceSha);
289
+ if (!policy.ok) return bad(policy.reason);
290
+ const survivors = listTree(git, commit).filter((p) => isPublicExportExcluded(p, policy.excludes));
291
+ if (survivors.length > 0) return bad(`${survivors.length} excluded path(s) in the export commit`);
292
+ return { ok: true };
293
+ }
294
+
295
+ /**
296
+ * Build (or confirm) the curated public snapshot.
297
+ *
298
+ * @param {object} opts
299
+ * @param {GitRunner} opts.git
300
+ * @param {string} [opts.source='HEAD']
301
+ * @param {string} [opts.exportRef=EXPORT_REF] output ref only — never read as a parent
302
+ * @param {string} [opts.remote=MIRROR_REMOTE] remote whose `refs/heads/main` is the public main
303
+ * @param {string} [opts.mirrorRef=MIRROR_REF] local tracking ref that must equal the remote main
304
+ * @param {boolean} [opts.dryRun=false]
305
+ * @param {(ctx: { git: GitRunner, commit: string, excludes: string[] }) => { ok: boolean, reason?: string, findings?: string[] }} [opts.snapshotCheck]
306
+ * @returns {{ status: 'created'|'unchanged'|'dry-run'|'refused', sha?: string, reason?: string, findings?: string[], stats?: object, warnings?: string[] }}
307
+ */
308
+ export function buildGithubExport({
309
+ git,
310
+ source = 'HEAD',
311
+ exportRef = EXPORT_REF,
312
+ remote = MIRROR_REMOTE,
313
+ mirrorRef = MIRROR_REF,
314
+ dryRun = false,
315
+ snapshotCheck = defaultSnapshotCheck,
316
+ }) {
317
+ const refuse = (reason, extra = {}) => ({ status: /** @type {const} */ ('refused'), reason, ...extra });
318
+
319
+ const integrity = graphIntegrityProblem(git);
320
+ if (integrity) return refuse(integrity);
321
+
322
+ const sourceSha = resolveCommit(git, source);
323
+ if (!sourceSha) return refuse(`source '${source}' does not resolve to a commit`);
324
+
325
+ // The policy is read from the SOURCE commit, so what is excluded matches what is exported.
326
+ const policy = readSourcePolicy(git, sourceSha);
327
+ if (!policy.ok) return refuse(policy.reason);
328
+ const { excludes } = policy;
329
+
330
+ // A push URL that differs from the fetch URL makes the anchor below meaningless for
331
+ // the push. Publishing refuses; a dry-run publishes nothing, so it only reports it.
332
+ const pushProblem = pushUrlProblem(git, remote);
333
+ if (pushProblem && !dryRun) return refuse(pushProblem);
334
+
335
+ // The parent is ALWAYS the public GitHub main, anchored on the remote itself: a stale,
336
+ // rolled-back or hand-moved tracking ref must never decide what becomes the base.
337
+ const main = verifiedPublicMain(git, remote, mirrorRef);
338
+ if (!main.ok) return refuse(main.reason);
339
+ const parentSha = main.sha;
340
+ const exportOld = resolveCommit(git, exportRef);
341
+ const warnings = pushProblem ? [pushProblem] : [];
342
+
343
+ // The published snapshot must describe an older state of the same source line.
344
+ const parentBody = git(['cat-file', 'commit', parentSha]);
345
+ if (parentBody.status !== 0) return refuse('parent commit unreadable');
346
+ const parentSource = parseSourceCommitTrailer(parentBody.stdout);
347
+ if (parentSource && !(resolveCommit(git, parentSource) && isAncestor(git, parentSource, sourceSha))) {
348
+ return refuse(`the parent's ${SOURCE_COMMIT_TRAILER} is not an ancestor of the source`);
349
+ }
350
+
351
+ // One selection path for export and snapshot probe (#1712); the helper post-checks
352
+ // the written tree for surviving excluded paths.
353
+ const snap = writeSnapshotTree({ git, sourceSha, excludes });
354
+ if (!snap.ok) return refuse(snap.reason);
355
+ const { tree: newTree, sourceFiles: sourceCount, removed } = snap;
356
+
357
+ const parentTree = git(['rev-parse', `${parentSha}^{tree}`]).stdout.trim();
358
+ const diff = git(['diff-tree', '-r', '--name-only', '-z', parentTree, newTree]);
359
+ const stats = {
360
+ sourceFiles: sourceCount,
361
+ excludedFiles: removed.length,
362
+ snapshotFiles: sourceCount - removed.length,
363
+ changedVsParent: diff.stdout.split('\0').filter(Boolean).length,
364
+ };
365
+
366
+ // Nothing to publish: the public main already carries this tree. The export ref is
367
+ // left alone; the caller pushes nothing.
368
+ if (newTree === parentTree) {
369
+ if (dryRun) return { status: 'dry-run', sha: parentSha, stats: { ...stats, unchanged: true }, warnings };
370
+ return { status: 'unchanged', sha: parentSha, stats };
371
+ }
372
+
373
+ const message = `Public snapshot of ${sourceSha.slice(0, 12)}\n\n${SOURCE_COMMIT_TRAILER}: ${sourceSha}\n`;
374
+ const ct = git(['commit-tree', newTree, '-p', parentSha, '-F', '-'], { input: message });
375
+ if (ct.status !== 0) return refuse('commit-tree failed');
376
+ const newCommit = ct.stdout.trim();
377
+
378
+ const check = snapshotCheck({ git, commit: newCommit, excludes });
379
+ if (!check.ok) return refuse(check.reason ?? 'snapshot check failed', { findings: check.findings, stats });
380
+
381
+ if (dryRun) return { status: 'dry-run', sha: newCommit, stats, warnings };
382
+
383
+ if (git(['update-ref', exportRef, newCommit, exportOld ?? '0'.repeat(sourceSha.length)]).status !== 0) {
384
+ return refuse(`update-ref ${exportRef} lost a compare-and-swap race`);
385
+ }
386
+ return { status: 'created', sha: newCommit, stats };
387
+ }
388
+
389
+ /**
390
+ * The repo's `mirror` Session Config value, read the way session-end § 4.4 reads it:
391
+ * `parse-config.mjs` run inside the repo, `mirror` taken from its JSON (`null` = key absent).
392
+ * Unreadable config, non-JSON output and unknown values are refusals, never a default.
393
+ * @param {string} repoRoot
394
+ * @returns {{ ok: true, mirror: 'none'|'github'|'curated'|null } | { ok: false, reason: string }}
395
+ */
396
+ export function readMirrorSetting(repoRoot) {
397
+ const res = spawnSync(process.execPath, [PARSE_CONFIG_SCRIPT], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
398
+ if (res.status !== 0) return { ok: false, reason: "Session Config unreadable — 'mirror' not evaluated" };
399
+ let mirror;
400
+ try {
401
+ mirror = JSON.parse(res.stdout).mirror ?? null;
402
+ } catch {
403
+ return { ok: false, reason: 'parse-config output is not JSON' };
404
+ }
405
+ if (mirror !== null && !MIRROR_VALUES.includes(mirror)) {
406
+ return { ok: false, reason: `unknown 'mirror: ${mirror}' (expected none | github | curated)` };
407
+ }
408
+ return { ok: true, mirror };
409
+ }
410
+
411
+ /**
412
+ * Fail-closed guard for every PLAIN push of source refs to the mirror (`git push github
413
+ * HEAD`/`main`, i.e. `mirror` absent or `github`). The working tree's Session Config can
414
+ * predate `mirror: curated` — any branch or checkout cut before the repo adopted the
415
+ * curated export has no `mirror` key and would publish the full private history. So the
416
+ * policy file decides, not the key: when HEAD or origin's main (read live via
417
+ * `ls-remote`, fetched when the object is not local) carries
418
+ * `.orchestrator/policy/public-export.json`, the repo publishes curated only and the
419
+ * plain push is refused. Without an `origin` remote there is no upstream to consult, so
420
+ * a `github` remote is pushed to only when `mirror: github` is set explicitly
421
+ * (`explicitGithub`); an absent key is "could not tell". "Could not tell" (origin
422
+ * unreadable, its main not fetchable) refuses as well.
423
+ * @param {{ git: GitRunner, upstream?: string, explicitGithub?: boolean }} opts
424
+ * @returns {{ ok: true } | { ok: false, reason: string }}
425
+ */
426
+ export function plainPushGuard({ git, upstream = 'origin', explicitGithub = false }) {
427
+ const adopted = (where) => ({
428
+ ok: /** @type {const} */ (false),
429
+ reason: `${where} carries ${PUBLIC_EXPORT_POLICY_FILE} — this repo publishes to GitHub only as a curated export; set 'mirror: curated' (or merge ${upstream}'s main) instead of pushing source history`,
430
+ });
431
+ if (git(['cat-file', '-e', `HEAD:${PUBLIC_EXPORT_POLICY_FILE}`]).status === 0) return adopted('HEAD');
432
+ const remotes = git(['remote']);
433
+ if (remotes.status !== 0) return { ok: false, reason: 'git remote failed — upstream policy unknown' };
434
+ const names = remotes.stdout.split('\n');
435
+ if (!names.includes(upstream)) {
436
+ if (!names.includes(MIRROR_REMOTE) || explicitGithub) return { ok: true };
437
+ return { ok: false, reason: `no '${upstream}' remote — the upstream export policy cannot be checked; set 'mirror: github' explicitly to push source history to '${MIRROR_REMOTE}'` };
438
+ }
439
+ const ls = git(['ls-remote', upstream, 'refs/heads/main']);
440
+ if (ls.status !== 0) return { ok: false, reason: `cannot read ${upstream}'s main — upstream export policy unknown` };
441
+ const sha = (ls.stdout.split('\n').find((l) => l.endsWith('\trefs/heads/main')) ?? '').split('\t')[0];
442
+ if (!sha) return { ok: true };
443
+ if (!resolveCommit(git, sha)) git(['fetch', '--quiet', '--no-tags', upstream, 'refs/heads/main']);
444
+ if (resolveCommit(git, sha) !== sha) return { ok: false, reason: `${upstream}'s main ${sha.slice(0, 12)} is not available locally — upstream export policy unknown` };
445
+ if (git(['cat-file', '-e', `${sha}:${PUBLIC_EXPORT_POLICY_FILE}`]).status === 0) return adopted(`${upstream}'s main`);
446
+ return { ok: true };
447
+ }
@@ -17,9 +17,11 @@
17
17
  * -h, --help Show usage and exit 0
18
18
  *
19
19
  * Exit codes (per cli-design.md):
20
- * 0 Success (or disabled / no-repos)
21
- * 1 User / input error (bad args, repo not found, config issue)
22
- * 2 System error (vault-dir not configured, strict-mode failures)
20
+ * 0 Success (or enabled: false / mode: off / no-repos)
21
+ * 1 User / input error (bad args, --repo names no discovered repo)
22
+ * 2 System error (Session Config unreadable, vault-dir not configured or
23
+ * outside the home directory, repo discovery or write failed, repo fetch
24
+ * failures under mode: strict)
23
25
  *
24
26
  * All dependencies are injectable via the second argument for testability.
25
27
  */
@@ -166,7 +168,7 @@ FLAGS
166
168
  -h, --help Show this help text and exit.
167
169
 
168
170
  EXIT CODES
169
- 0 Success (or feature disabled / no repos configured)
171
+ 0 Success (or enabled: false / mode: off / no repos configured)
170
172
  1 User / input error (bad args, repo filter matched nothing)
171
173
  2 System error (vault-dir not configured, strict-mode repo failures)
172
174
  `);
@@ -188,7 +190,7 @@ EXIT CODES
188
190
  return { exitCode: 2, action: 'error', reposScanned: 0, reposFailed: 0 };
189
191
  }
190
192
 
191
- // ── Check enabled flag ─────────────────────────────────────────────────────
193
+ // ── Check enabled flag + mode gate ─────────────────────────────────────────
192
194
  const portfolioConfig = config['gitlab-portfolio'] ?? {};
193
195
  if (portfolioConfig.enabled === false) {
194
196
  if (!flagJson) {
@@ -198,6 +200,15 @@ EXIT CODES
198
200
  }
199
201
  return { exitCode: 0, action: 'disabled', reposScanned: 0, reposFailed: 0 };
200
202
  }
203
+ if (portfolioConfig.mode === 'off') {
204
+ // #1619: mode: off is a no-op like enabled: false — before any vault/repo work.
205
+ if (!flagJson) {
206
+ process.stdout.write(`gitlab-portfolio: mode: off — skipped (set gitlab-portfolio.mode: warn|strict to run)\n`);
207
+ } else {
208
+ process.stdout.write(JSON.stringify({ action: 'disabled', reposScanned: 0, reposFailed: 0 }) + '\n');
209
+ }
210
+ return { exitCode: 0, action: 'disabled', reposScanned: 0, reposFailed: 0 };
211
+ }
201
212
 
202
213
  // ── Resolve vault-dir ──────────────────────────────────────────────────────
203
214
  const vaultIntegration = config['vault-integration'] ?? {};
@@ -0,0 +1,159 @@
1
+ /**
2
+ * handover-queue-marks.mjs — pure core of the handover queue check (#1639).
3
+ *
4
+ * A fleet session writes its open points into a handover or train manifest
5
+ * (`skills/_shared/fleet-protocol.md` § Übergabe und Zug-Manifest). A point
6
+ * only reaches the coordinator's owner-queue when it carries a mark: measured
7
+ * 2026-10-15, 35 of 111 open points (32 %) were recorded nowhere. This module
8
+ * finds the unmarked points and inserts marks; the CLI
9
+ * (`scripts/handover-queue-check.mjs`) files the queue items. No I/O here.
10
+ *
11
+ * Contract:
12
+ * - Open-point sections: a `##`–`####` heading whose text starts with
13
+ * `Offen`, `Nachkontrolle`, `Folge` or `Danach` (case-insensitive) and
14
+ * does not contain `erledigt`. Deeper headings inside it inherit it unless
15
+ * they say `erledigt`; the section ends at the next heading of the same or
16
+ * a higher level.
17
+ * - A point is a top-level list item (`- `, `* `, `1. `) in such a section;
18
+ * indented lines (continuation, nested bullets) belong to it.
19
+ * - A point is marked when its text carries `queue:#<id>` or `issue:#<iid>`,
20
+ * and done when it starts with `[x]`, `~~` or `Erledigt`. Everything else
21
+ * is unmarked.
22
+ * - CRLF files parse like LF files; inserted marks keep the line ending.
23
+ */
24
+
25
+ const HEADING_RE = /^(#{1,6})\s+(.*)$/;
26
+ const OPEN_SECTION_RE = /^(offen|nachkontrolle|folge|danach)/i;
27
+ const DONE_TITLE_RE = /erledigt/i;
28
+ // Owner decisions need options and a class the text does not carry (#1639 F3).
29
+ // Narrow on purpose: "Offen (owner-queue, wartet auf Lead)" or "Owner-Direktive"
30
+ // are lead work and must still be filed.
31
+ const OWNER_SECTION_RE = /owner-?(?:entscheid|frage)|\bentscheide?\b|(?:beim|an den|für den) owner/i;
32
+ const ITEM_RE = /^(?:[-*]|\d+[.)])\s+(.*)$/;
33
+ const MARK_RE = /\b(?:queue|issue):#\d+\b/;
34
+ const DONE_RE = /^(?:\[[xX]\]|~~|erledigt\b)/i;
35
+ const FENCE_RE = /^\s*(```|~~~)/;
36
+ // The coordinator CLI's `--ref` shape (lead-session-orchestrator
37
+ // src/owner-queue/typen.ts): a bare repo name, no group path, iid ≥ 1.
38
+ const CLI_REF_RE = /^[A-Za-z0-9._-]{1,100}[!#][1-9]\d{0,8}$/;
39
+
40
+ /**
41
+ * @typedef {object} OpenPoint
42
+ * @property {number} line 0-based index of the item's first line
43
+ * @property {string} section heading text of the enclosing open section
44
+ * @property {string} text item text, continuation lines joined by spaces
45
+ * @property {boolean} marked
46
+ * @property {boolean} done
47
+ */
48
+
49
+ /**
50
+ * Lists every top-level list item in an open-point section.
51
+ *
52
+ * @param {string} content - raw markdown
53
+ * @returns {OpenPoint[]}
54
+ */
55
+ export function findOpenPoints(content) {
56
+ const lines = content.split('\n').map((l) => l.replace(/\r$/, ''));
57
+ /** @type {OpenPoint[]} */
58
+ const points = [];
59
+ let root = null; // { level, title } of the enclosing open section
60
+ let active = false; // inside root and not in an `erledigt` subsection
61
+ let current = null;
62
+ let inFence = false;
63
+
64
+ const close = () => {
65
+ if (current) points.push(current);
66
+ current = null;
67
+ };
68
+
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const line = lines[i];
71
+ if (FENCE_RE.test(line)) inFence = !inFence;
72
+ if (inFence) continue;
73
+ const heading = HEADING_RE.exec(line);
74
+ if (heading) {
75
+ close();
76
+ const level = heading[1].length;
77
+ const title = heading[2].trim();
78
+ if (root && level > root.level) {
79
+ active = !DONE_TITLE_RE.test(title);
80
+ } else {
81
+ const open = level >= 2 && level <= 4 && OPEN_SECTION_RE.test(title) && !DONE_TITLE_RE.test(title);
82
+ root = open ? { level, title } : null;
83
+ active = open;
84
+ }
85
+ continue;
86
+ }
87
+ if (!active) continue;
88
+ const item = ITEM_RE.exec(line);
89
+ if (item) {
90
+ close();
91
+ current = { line: i, section: root.title, text: item[1].trim(), marked: false, done: false };
92
+ } else if (current && /^\s+\S/.test(line)) {
93
+ current.text += ` ${line.trim()}`;
94
+ } else if (current && line.trim() !== '') {
95
+ close(); // unindented prose ends the item
96
+ }
97
+ }
98
+ close();
99
+
100
+ for (const p of points) {
101
+ p.marked = MARK_RE.test(p.text);
102
+ p.done = DONE_RE.test(p.text);
103
+ }
104
+ return points;
105
+ }
106
+
107
+ /** Points that still need a queue item. */
108
+ export function unmarkedPoints(points) {
109
+ return points.filter((p) => !p.marked && !p.done);
110
+ }
111
+
112
+ /**
113
+ * Queue item kind for a point: a post-merge/train check is `nachsorge`,
114
+ * other open points `lead-todo`. `null` for a point under an owner/decision
115
+ * heading — an `entscheid` needs options and a class, so it is reported, never
116
+ * filed as a lead todo. Peer promises (`zusage`) likewise stay the session's
117
+ * own call under fleet-protocol Schritt 1a.
118
+ *
119
+ * @param {OpenPoint} point
120
+ * @returns {'nachsorge'|'lead-todo'|null}
121
+ */
122
+ export function kindFor(point) {
123
+ if (OWNER_SECTION_RE.test(point.section)) return null;
124
+ return /^nachkontrolle/i.test(point.section) ? 'nachsorge' : 'lead-todo';
125
+ }
126
+
127
+ /**
128
+ * `--ref` for owner-queue: the first MR (`!N`) or issue (`#N`) the point names.
129
+ * A `group/repo!N` keeps only its last path segment; a bare `!N` gets `repo`.
130
+ * `null` when nothing is named or the result would not pass the CLI's ref
131
+ * check — a missing ref files the item, an invalid one would refuse it.
132
+ *
133
+ * @param {OpenPoint} point
134
+ * @param {string} repo
135
+ * @returns {string|null}
136
+ */
137
+ export function refFor(point, repo) {
138
+ const m = /(?:^|[\s(*`])((?:[\w.-]+\/)*([\w.-]+))?([!#])(\d+)\b/.exec(point.text.replace(MARK_RE, ''));
139
+ if (!m) return null;
140
+ const ref = `${m[2] ?? repo}${m[3]}${m[4]}`;
141
+ return CLI_REF_RE.test(ref) ? ref : null;
142
+ }
143
+
144
+ /**
145
+ * Appends ` queue:#<id>` to the first line of each point in `ids`
146
+ * (Map<line, id>), keeping a CRLF line ending. Other lines are untouched.
147
+ *
148
+ * @param {string} content
149
+ * @param {Map<number, number>} ids
150
+ * @returns {string}
151
+ */
152
+ export function insertMarks(content, ids) {
153
+ const lines = content.split('\n');
154
+ for (const [line, id] of ids) {
155
+ const cr = lines[line].endsWith('\r') ? '\r' : '';
156
+ lines[line] = `${lines[line].replace(/\s+$/, '')} queue:#${id}${cr}`;
157
+ }
158
+ return lines.join('\n');
159
+ }