session-orchestrator 5.12.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 (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  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 +162 -4
  11. package/README.md +126 -127
  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 +1 -1
  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 +90 -17
  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 +49 -15
  47. package/docs/session-config-template.md +30 -22
  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 +139 -49
  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 -2
  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 +39 -27
  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/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -53,6 +53,7 @@ import {
53
53
  _setResolverForTest,
54
54
  } from './lib/vault-relocation-rules.mjs';
55
55
  import { buildBackfillIndex, parseSessionId } from './lib/vault-repo-backfill.mjs';
56
+ import { noteIdMatches } from './lib/vault-mirror/utils.mjs';
56
57
  import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
57
58
  import { isMainModule } from './lib/is-main-module.mjs';
58
59
  import { atomicWriteText, envelopeToError } from './lib/io.mjs';
@@ -358,7 +359,7 @@ async function buildCrossRepoIndices(reposRoot) {
358
359
  * Only UNANIMOUS mappings are learned; an identity whose source-repo:/repo:
359
360
  * sessions disagree on the namespace is left unmapped (ambiguous → no remap).
360
361
  *
361
- * @param {{ sidIndex: Map<string, Set<string>>, parsedVaultSessions: Array<{id:string, frontmatter:object}> }} args
362
+ * @param {{ sidIndex: Map<string, Set<string>>, parsedVaultSessions: Array<{id:string, sid?:string, frontmatter:object}> }} args
362
363
  * @returns {Map<string, string>} identity → canonical namespace
363
364
  */
364
365
  function learnCanonicalMap({ sidIndex, parsedVaultSessions }) {
@@ -366,11 +367,11 @@ function learnCanonicalMap({ sidIndex, parsedVaultSessions }) {
366
367
  // 'AcmeWidgetV2' → 'acmewidgetv2' === 'acme-widget-v2' → 'acmewidgetv2'.
367
368
  const normalize = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
368
369
  const votes = new Map(); // identity → Map<canonicalNs, count>
369
- for (const { id, frontmatter } of parsedVaultSessions) {
370
+ for (const { id, sid = id, frontmatter } of parsedVaultSessions) {
370
371
  if (!id || !frontmatter || !hasSessionRepoSignal(frontmatter)) continue;
371
372
  const { namespace } = namespaceForSession(frontmatter); // source-repo:/repo: → canonical
372
373
  if (!isConfident(namespace)) continue; // skip _unsorted/redacted-repo/unknown-repo
373
- const identities = sidIndex.get(id);
374
+ const identities = sidIndex.get(sid);
374
375
  if (!identities) continue;
375
376
  // Only an UNAMBIGUOUS source-repo:/repo: note teaches: if its id appears in >1 repo's jsonl,
376
377
  // it would mis-teach every other identity its own namespace (collision pollution).
@@ -478,11 +479,14 @@ async function collectParsedVaultSessions(sessionsDir) {
478
479
  continue;
479
480
  }
480
481
  const frontmatter = parseRelocationFrontmatter(content);
482
+ const stem = path.basename(filePath).replace(/\.md$/, '');
481
483
  // Prefer the frontmatter id; fall back to the basename (the session id by convention).
482
- const id =
483
- (typeof frontmatter.id === 'string' && frontmatter.id.trim()) ||
484
- path.basename(filePath).replace(/\.md$/, '');
485
- if (id) out.push({ id, frontmatter });
484
+ const id = (typeof frontmatter.id === 'string' && frontmatter.id.trim()) || stem;
485
+ // #1602: a namespaced id (`<source-repo>-<session id>`) is no session id; the
486
+ // cross-repo index joins on the file stem it names. `id` stays the backfill
487
+ // key, because namespaceForSession looks that index up by `frontmatter.id`.
488
+ const sid = noteIdMatches(id, stem, frontmatter['source-repo']) ? stem : id;
489
+ if (id) out.push({ id, sid, frontmatter });
486
490
  }
487
491
 
488
492
  return out;
@@ -47,7 +47,7 @@ import { parseArgs } from 'node:util';
47
47
  import path from 'node:path';
48
48
 
49
49
  import { repairLedger, CANONICAL_LEDGER_REL } from './lib/session-record-repair.mjs';
50
- import { getProjectDir } from './lib/platform.mjs';
50
+ import { getProjectDir, resolveEventsLedgerRoot } from './lib/platform.mjs';
51
51
  import { isMainModule } from './lib/is-main-module.mjs';
52
52
 
53
53
  const USAGE =
@@ -60,7 +60,8 @@ const USAGE =
60
60
  CANONICAL_LEDGER_REL +
61
61
  ');\n' +
62
62
  ' must resolve inside <repo-root>/.orchestrator/metrics/\n' +
63
- ' --repo-root project root (default: resolved project dir)\n' +
63
+ ' --repo-root project root (default: the resolved project dir\'s ledger root —\n' +
64
+ ' its main checkout when run from a linked worktree)\n' +
64
65
  'Exit codes: 0 completed, 1 arg error, 2 system error, 3 post-verification failed\n' +
65
66
  ' (backup restored), 4 unrepairable remain (N lines kept byte-identical;\n' +
66
67
  ' every repairable line fixed — dry-run: would remain)\n';
@@ -77,6 +78,7 @@ const USAGE =
77
78
  * @returns {{ok: true, file: string}|{ok: false, metricsDir: string, resolved: string}}
78
79
  */
79
80
  function resolveLedgerTarget(file, repoRoot) {
81
+ // ledger-root: explicit --repo-root by design; pre-bash-sessions-ledger-guard judges the same root
80
82
  const metricsDir = path.resolve(repoRoot, '.orchestrator', 'metrics');
81
83
  // `absFile` (normalized, `..` collapsed) is what the repair uses — NOT the
82
84
  // symlink-resolved form, so the canonical-path equality in `verifyWritten`
@@ -108,6 +110,44 @@ function resolveLedgerTarget(file, repoRoot) {
108
110
  return { ok: true, file: absFile };
109
111
  }
110
112
 
113
+ /**
114
+ * Refuse an explicit `--repo-root` whose ledger path follows a symlink (#1665).
115
+ * The ledger guard admits the absolute `--apply` form only after
116
+ * `isSymlinkFreeAbsolute` (hooks/pre-bash-sessions-ledger-guard.mjs) accepted
117
+ * the root and the ledger, but it checks BEFORE this process runs, and
118
+ * {@link resolveLedgerTarget} follows symlinks via `realpathSync` — a link
119
+ * swapped in between the two would redirect the repair (TOCTOU).
120
+ *
121
+ * Same rule as the hook: NO component of the path may be a symlink — the root,
122
+ * every ancestor (so a root under macOS `/tmp` → `/private/tmp` is refused,
123
+ * exactly as the hook refuses it), `.orchestrator/`, `metrics/` and the ledger
124
+ * file — and the root must equal its own `realpath`. `lstat` judges the link
125
+ * itself, never its target. One deliberate difference: a MISSING component is
126
+ * not a symlink here; the repair reports it as the I/O error it is.
127
+ *
128
+ * @param {string} repoRoot - the explicit `--repo-root` value
129
+ * @param {string} ledgerFile - the requested ledger path under that root
130
+ * @returns {string|null} the reason to refuse, or null
131
+ */
132
+ function symlinkRefusal(repoRoot, ledgerFile) {
133
+ for (let current = path.resolve(ledgerFile); ; current = path.dirname(current)) {
134
+ try {
135
+ if (fs.lstatSync(current).isSymbolicLink()) return `${current} is a symlink`;
136
+ } catch {
137
+ /* absent — nothing to follow */
138
+ }
139
+ if (path.dirname(current) === current) break;
140
+ }
141
+ const root = path.resolve(repoRoot);
142
+ try {
143
+ const real = fs.realpathSync.native(root);
144
+ if (real !== root) return `${root} resolves to ${real}`;
145
+ } catch {
146
+ /* absent — the repair reports the missing ledger */
147
+ }
148
+ return null;
149
+ }
150
+
111
151
  /**
112
152
  * Human-readable summary — mirrors `scripts/backfill-abandoned-sessions.mjs`
113
153
  * `renderHuman()` so the two migration CLIs read the same way.
@@ -123,6 +163,9 @@ export function renderHuman(s) {
123
163
  lines.push(` invalid before: ${s.invalid_before}`);
124
164
  lines.push(` ${s.mode === 'apply' ? 'repaired: ' : 'would repair: '} ${s.repaired}`);
125
165
  lines.push(` invalid after: ${s.invalid_after}${s.mode === 'apply' ? '' : ' (projected)'}`);
166
+ if (s.healed_valid > 0) {
167
+ lines.push(` healed (valid): ${s.healed_valid}`);
168
+ }
126
169
  if (s.unparseable > 0) {
127
170
  lines.push(` unparseable: ${s.unparseable} (passed through untouched)`);
128
171
  }
@@ -148,6 +191,10 @@ export function renderHuman(s) {
148
191
  }
149
192
  if (s.ok === false) {
150
193
  lines.push(' POST-VERIFICATION FAILED — backup restored, ledger unchanged.');
194
+ // #1670: lines appended after the rename are kept, not rolled back with the repair.
195
+ if (s.restore_appended_bytes > 0) {
196
+ lines.push(` kept: ${s.restore_appended_bytes} byte(s) appended concurrently after the rename`);
197
+ }
151
198
  } else if (s.errors.length > 0) {
152
199
  lines.push(
153
200
  s.mode === 'apply'
@@ -201,9 +248,27 @@ async function main() {
201
248
 
202
249
  // --apply is an explicit opt-in; absent it (or with --dry-run) we never write.
203
250
  const apply = values.apply === true && values['dry-run'] !== true;
204
- const repoRoot = values['repo-root'] || getProjectDir();
251
+ // #1592: only the DEFAULT follows the ledger to the main checkout, where
252
+ // every writer appends. An explicit --repo-root stays the literal target: the
253
+ // ledger guard sanctions the absolute --apply form only for a --repo-root
254
+ // equal to the measured Git top-level and outside the resolved vault — mapping
255
+ // it would let a linked worktree of the vault write the vault's main ledger.
256
+ const repoRoot = values['repo-root'] || resolveEventsLedgerRoot(getProjectDir());
205
257
  const requestedFile = values.file || path.join(repoRoot, CANONICAL_LEDGER_REL);
206
258
 
259
+ // #1665: an explicit root is the whole target identity — never follow a
260
+ // symlink anywhere on the ledger path, checked BEFORE any I/O (dry-run too).
261
+ if (values['repo-root']) {
262
+ const refusal = symlinkRefusal(repoRoot, requestedFile);
263
+ if (refusal) {
264
+ process.stderr.write(
265
+ `repair-invalid-sessions: refusing --repo-root ${repoRoot}: ${refusal} ` +
266
+ `(the ledger path under an explicit --repo-root must not follow symlinks — #1665)\n`
267
+ );
268
+ process.exit(1);
269
+ }
270
+ }
271
+
207
272
  // MED-3: bound the target to <repoRoot>/.orchestrator/metrics/ BEFORE any I/O.
208
273
  const target = resolveLedgerTarget(requestedFile, repoRoot);
209
274
  if (!target.ok) {
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /** Stdin-only boundary for commands/session.md; never executes task context. */
3
3
  import { accessSync, constants, readFileSync, statSync } from 'node:fs';
4
- import { join, resolve } from 'node:path';
4
+ import { join } from 'node:path';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { resolveSessionInvocation } from './lib/session-invocation.mjs';
7
- import { readCanonicalSessions } from './lib/sessions-canonical.mjs';
8
- import { isRealSession } from './lib/session-schema/filters.mjs';
7
+ import { readCanonicalSessions, isMeasuredSession } from './lib/sessions-canonical.mjs';
9
8
  import { resolveEventsLedgerRoot } from './lib/platform.mjs';
10
9
 
11
10
  /**
12
11
  * One ledger: `false` when absent, `undefined` when present but not a readable
13
- * file, else whether it holds a real session.
12
+ * file, else whether it holds a measured session.
14
13
  *
15
14
  * @param {string} root
16
15
  * @returns {boolean|undefined}
17
16
  */
18
17
  function ledgerHasHistory(root) {
18
+ // ledger-root: root is resolveEventsLedgerRoot-mapped by the only caller, measureHistory
19
19
  const file = join(root, '.orchestrator', 'metrics', 'sessions.jsonl');
20
20
  try {
21
21
  if (!statSync(file).isFile()) return undefined;
@@ -23,28 +23,21 @@ function ledgerHasHistory(root) {
23
23
  } catch (error) {
24
24
  return error?.code === 'ENOENT' ? false : undefined;
25
25
  }
26
- return readCanonicalSessions({ filePath: file }).some(isRealSession);
26
+ return readCanonicalSessions({ filePath: file }).some(isMeasuredSession);
27
27
  }
28
28
 
29
29
  /**
30
- * Whether the repo holds a real session of its own: `true`/`false`, or
30
+ * Whether the repo holds a measured session of its own: `true`/`false`, or
31
31
  * `undefined` when that cannot be measured. sessions.jsonl is gitignored, so a
32
- * linked worktree also reads its main checkout's ledger (`resolveEventsLedgerRoot`);
33
- * one with history in either place has history. A ledger that exists but
32
+ * linked worktree reads its main checkout's ledger (`resolveEventsLedgerRoot`),
33
+ * never a worktree-local shadow. A ledger that exists but
34
34
  * cannot be read is "not measured", never "no history".
35
35
  *
36
36
  * @param {string} repoRoot
37
37
  * @returns {boolean|undefined}
38
38
  */
39
39
  function measureHistory(repoRoot) {
40
- const roots = [...new Set([resolve(repoRoot), resolveEventsLedgerRoot(repoRoot)])];
41
- let unmeasured = false;
42
- for (const root of roots) {
43
- const has = ledgerHasHistory(root);
44
- if (has === true) return true;
45
- if (has === undefined) unmeasured = true;
46
- }
47
- return unmeasured ? undefined : false;
40
+ return ledgerHasHistory(resolveEventsLedgerRoot(repoRoot));
48
41
  }
49
42
 
50
43
  const HELP = `Usage: node scripts/resolve-session-invocation.mjs [--json] [--repo-root <dir>] < arguments.txt
@@ -54,7 +47,7 @@ Resolve the leading housekeeping, feature, deep or ultradeep token; preserve
54
47
  the rest as task context. Empty input defaults to deep. An invalid mode emits
55
48
  a warning and resolves to deep, as the session command has always specified.
56
49
  A bare /session in a repo whose .orchestrator/metrics/sessions.jsonl holds no
57
- real session adds defaultReason "no-session-history": ask for task and mode
50
+ measured session adds defaultReason "no-session-history": ask for task and mode
58
51
  before starting. A linked git worktree reads its main checkout's ledger; an
59
52
  unreadable ledger counts as not measured and adds nothing.
60
53
 
@@ -30,6 +30,13 @@
30
30
  * 2 I/O error — incl. a store lock not acquired under --apply: that repo
31
31
  * is left unwritten and reported `status: 'error'`, the run continues
32
32
  * with the next repo, and exits 2 at the end
33
+ * 3 --apply on a host whose learnings-store role is `inbox` (#1516): no
34
+ * store is rewritten; every repo is still migrated in memory and reported
35
+ * `status: 'refused-store-role'`. An I/O error (2) takes precedence.
36
+ * 4 --apply on a host whose learnings-store role is INVALID (#1516: set but
37
+ * not writer|inbox, or owner.yaml unreadable/unparseable): no store is
38
+ * rewritten; every repo is still migrated in memory and reported
39
+ * `status: 'refused-store-role-invalid'`. An I/O error (2) takes precedence.
33
40
  */
34
41
 
35
42
  import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
@@ -44,6 +51,7 @@ import { LearningsLockError, withLearningsLock } from './lib/learnings/io.mjs';
44
51
  import { getCrossRepoProjects, getConfinementRoot } from './lib/config/cross-repo.mjs';
45
52
  import { validatePathInsideProject } from './lib/path-utils.mjs';
46
53
  import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
54
+ import { resolveStoreRole } from './lib/learnings/store-role.mjs';
47
55
 
48
56
  const LEARNINGS_REL = '.orchestrator/metrics/learnings.jsonl';
49
57
 
@@ -65,6 +73,11 @@ Options:
65
73
  --out <path> Write output to file instead of stdout.
66
74
 
67
75
  Exit codes: 0 success 1 input error 2 I/O error
76
+ 3 --apply refused: this host's learnings-store role is inbox (#1516),
77
+ nothing written (repos reported status refused-store-role)
78
+ 4 --apply refused: this host's learnings-store role is invalid (#1516:
79
+ SO_LEARNINGS_STORE_ROLE or owner.yaml learnings.store-role set but
80
+ unusable), nothing written (status refused-store-role-invalid)
68
81
  `);
69
82
  process.exit(0);
70
83
  }
@@ -141,7 +154,10 @@ if (reposArg) {
141
154
  /**
142
155
  * @typedef {Object} RepoResult
143
156
  * @property {string} repo - absolute repo path
144
- * @property {string} status - 'skipped' | 'dry-run' | 'applied' | 'error'
157
+ * @property {string} status - 'skipped' | 'dry-run' | 'applied' | 'error' |
158
+ * 'refused-store-role' (--apply on an inbox-role host, #1516: migrated in
159
+ * memory, nothing written) | 'refused-store-role-invalid' (--apply while the
160
+ * host's store role is invalid, #1516: likewise nothing written)
145
161
  * @property {number} total - total records parsed
146
162
  * @property {number} invalidPre - invalid records before migration
147
163
  * @property {number} invalidPost - invalid records after migration
@@ -200,6 +216,14 @@ function emptyResult(repoPath, status, error) {
200
216
  /** Repos whose store lock was not acquired under --apply (nothing written there). */
201
217
  const lockFailedRepos = [];
202
218
 
219
+ // #1516: an inbox-role host never rewrites a canonical learnings.jsonl. The role
220
+ // is host-wide (env / owner.yaml), so it is resolved once for every repo.
221
+ // Fail-closed: an INVALID role (set but unusable) refuses as well, with its
222
+ // own status and exit code — only an unset role means writer.
223
+ const storeRole = applyFlag ? resolveStoreRole() : null;
224
+ const refuseStoreWrites = storeRole?.role === 'inbox';
225
+ const refuseInvalidRole = storeRole?.role === 'invalid';
226
+
203
227
  /**
204
228
  * Process a single repo. Returns a RepoResult.
205
229
  *
@@ -216,6 +240,18 @@ async function processRepo(repoPath, apply) {
216
240
  const learningsPath = join(repoPath, LEARNINGS_REL);
217
241
 
218
242
  if (!existsSync(learningsPath)) return emptyResult(repoPath, 'skipped', null);
243
+ if (apply && refuseStoreWrites) {
244
+ const res = migrateStore(repoPath, learningsPath, false);
245
+ return res.status === 'error'
246
+ ? res
247
+ : { ...res, status: 'refused-store-role', error: 'store-role inbox: write skipped' };
248
+ }
249
+ if (apply && refuseInvalidRole) {
250
+ const res = migrateStore(repoPath, learningsPath, false);
251
+ return res.status === 'error'
252
+ ? res
253
+ : { ...res, status: 'refused-store-role-invalid', error: 'store-role invalid: write skipped' };
254
+ }
219
255
  if (!apply) return migrateStore(repoPath, learningsPath, false);
220
256
 
221
257
  try {
@@ -524,4 +560,23 @@ if (ioFailed.length > 0) {
524
560
  exitCode = 2;
525
561
  }
526
562
 
563
+ const refusedRepos = results.filter((r) => r.status === 'refused-store-role');
564
+ if (refusedRepos.length > 0) {
565
+ process.stderr.write(
566
+ `run-migrate-v2: learnings store role is inbox on this host (#1516) — --apply wrote nothing ` +
567
+ `in ${refusedRepos.length} repo(s); run it on the writer host\n`
568
+ );
569
+ if (exitCode === 0) exitCode = 3;
570
+ }
571
+
572
+ const invalidRoleRepos = results.filter((r) => r.status === 'refused-store-role-invalid');
573
+ if (invalidRoleRepos.length > 0) {
574
+ process.stderr.write(
575
+ `run-migrate-v2: learnings store role is invalid on this host (#1516: ${storeRole?.reason}) — ` +
576
+ `--apply wrote nothing in ${invalidRoleRepos.length} repo(s); fix owner.yaml learnings.store-role ` +
577
+ `or SO_LEARNINGS_STORE_ROLE\n`
578
+ );
579
+ if (exitCode === 0) exitCode = 4;
580
+ }
581
+
527
582
  process.exit(exitCode);
@@ -30,7 +30,7 @@
30
30
  * 0 would be a false green (Epic #1425 A3).
31
31
  *
32
32
  * The gate sub-scripts in scripts/lib/gates/ are NOT reimplemented here; they are
33
- * invoked via spawnInGroup() — a detached, process-group-leading shell — with the
33
+ * invoked via spawnInGroup() using argv (a detached group on POSIX) with the
34
34
  * required env vars.
35
35
  *
36
36
  * References:
@@ -59,7 +59,6 @@ import { resolveWaveNumber } from './lib/quality-gate-wave.mjs';
59
59
  import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
60
60
  import { gateKillFields } from './lib/quality-gate.mjs';
61
61
  import { isMainModule } from './lib/is-main-module.mjs';
62
- import { shellQuote } from './lib/sh-quote.mjs';
63
62
 
64
63
  // ---------------------------------------------------------------------------
65
64
  // Constants
@@ -226,6 +225,28 @@ function failedFilesFromGateStdout(stdout) {
226
225
  return named.length > 0 ? named : null;
227
226
  }
228
227
 
228
+ /**
229
+ * Lift `test.rerun` (`{ files, status }`) out of a gate sub-script's JSON
230
+ * stdout envelope (#1673). Only `gate-full.mjs` publishes it, and only when it
231
+ * re-ran timeout-only failures. `null` for every other case. Never throws.
232
+ *
233
+ * @param {string} stdout — the gate sub-script's captured stdout.
234
+ * @returns {{ files: string[], status: 'pass'|'fail' }|null}
235
+ */
236
+ function rerunFromGateStdout(stdout) {
237
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(stdout);
241
+ } catch {
242
+ return null;
243
+ }
244
+ const rerun = parsed?.test?.rerun;
245
+ if (!rerun || !Array.isArray(rerun.files) || (rerun.status !== 'pass' && rerun.status !== 'fail')) return null;
246
+ const files = rerun.files.filter((f) => typeof f === 'string' && f.trim());
247
+ return files.length > 0 ? { files, status: rerun.status } : null;
248
+ }
249
+
229
250
  /**
230
251
  * The reporting variants — they exit 0 even after a failing check, so their
231
252
  * event NAME never carries the check verdict (#1459). `full-gate` exits non-zero
@@ -379,7 +400,9 @@ async function main() {
379
400
  if (config) {
380
401
  if (existsSync(config)) {
381
402
  try {
382
- configJson = JSON.parse(readFileSync(config, 'utf8'));
403
+ // Windows PowerShell 5.1 `Set-Content -Encoding utf8` writes a BOM, which
404
+ // JSON.parse rejects; the gate then ran the npm defaults instead (#1612).
405
+ configJson = JSON.parse(readFileSync(config, 'utf8').replace(/^\uFEFF/, ''));
383
406
  } catch (err) {
384
407
  warn(`Could not parse config file '${config}': ${err.message}; using defaults`);
385
408
  }
@@ -496,9 +519,8 @@ async function main() {
496
519
  // Handing it a child with `stderr: 'inherit'` leaves `child.stderr` null, the
497
520
  // module's own `child.stderr?.on(…)` a no-op, and the gate's warnings live on
498
521
  // the operator's terminal exactly as before.
499
- // Quoted: the gate path derives from `import.meta.url`, so a space in the
500
- // checkout path must not split the command.
501
- const gateCommand = `node ${shellQuote(gatePath)}`;
522
+ // argv preserves spaces and apostrophes on POSIX and Windows.
523
+ const gateCommand = [process.execPath, gatePath];
502
524
  // Derived from the SAME resolved per-command ceiling published as
503
525
  // `GATE_TIMEOUT_MS` above — not from `resolveGateTimeoutMs()` a second time.
504
526
  // The outer cap must stay strictly ABOVE the inner one so the inner kill fires
@@ -531,7 +553,7 @@ async function main() {
531
553
  // terminal's foreground group: the first read from an inherited TTY earns it
532
554
  // SIGTTIN, which stops the whole group until the outer ceiling kills it. An
533
555
  // ignored stdin turns that hang into an immediate EOF.
534
- spawnFn: (command, options) => spawn(command, { ...options, stdio: ['ignore', 'pipe', 'inherit'] }),
556
+ spawnFn: (command, args, options) => spawn(command, args, { ...options, stdio: ['ignore', 'pipe', 'inherit'] }),
535
557
  });
536
558
 
537
559
  // `pid: -1` is `spawnInGroup`'s spawn-failure channel (it never rejects).
@@ -595,6 +617,9 @@ async function main() {
595
617
  // The names behind `counts.failed`. Absent, never `[]` — see
596
618
  // `failedFilesFromGateStdout`.
597
619
  const failedFiles = failedFilesFromGateStdout(gateStdout);
620
+ // The serial re-run of timeout-only failures (#1673): which files it named
621
+ // and how it ended, so the rate of load-rescued gates stays measurable.
622
+ const rerun = rerunFromGateStdout(gateStdout);
598
623
  // Wave-scope sidecar is read from the SAME project dir the event lands in
599
624
  // (emitEvent's own destination precedence), so a tmp-scoped run cannot pick
600
625
  // up the host repo's live wave. Mirrors the hook's projectDir resolution.
@@ -625,6 +650,7 @@ async function main() {
625
650
  ...(checkFailed !== null ? { check_failed: checkFailed } : {}),
626
651
  ...(counts ? { counts } : {}),
627
652
  ...(failedFiles ? { failed_files: failedFiles } : {}),
653
+ ...(rerun ? { rerun } : {}),
628
654
  ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
629
655
  ...sessionAttribution(ledgerRoot ?? repoRoot),
630
656
  },
@@ -25,9 +25,12 @@
25
25
  * 2026-10-03). The step refuses, naming the remove + add
26
26
  * commands, while ANY directory marketplace reads this clone;
27
27
  * GitHub/npm sources update as before, with a one-line hint.
28
- * 3. codex — only if the plugin is already installed in Codex: re-run
29
- * scripts/codex-install.mjs, which installs from the same
30
- * packed STAGE_DIR (#1518), never from this clone.
28
+ * 3. codex — only if the plugin is already installed in Codex (under any
29
+ * marketplace): re-run scripts/codex-install.mjs, which
30
+ * installs from the same packed STAGE_DIR (#1518), never from
31
+ * this clone. Any personal-marketplace `@local` install fails
32
+ * read-only with the cleanup steps, wherever its source points
33
+ * (#1608): codex-install would remove it.
31
34
  *
32
35
  * Never deletes an old plugin cache directory: the running session still
33
36
  * loads it until the harness restarts.
@@ -316,15 +319,61 @@ export function updateClaude({ run, log }, opts, ctx = {}) {
316
319
  };
317
320
  }
318
321
 
319
- function updateCodex({ run, log }, opts) {
320
- if (!commandOnPath('codex')) return { step: 'codex', status: 'skipped', detail: 'codex not on PATH' };
322
+ /**
323
+ * Installed session-orchestrator rows of the human `codex plugin list` table
324
+ * (`PLUGIN STATUS VERSION SOURCE`, columns padded with 2+ spaces). The id
325
+ * carries whatever marketplace installed it — `session-orchestrator@local`
326
+ * from the personal marketplace (#1608) — so the name must end at `@` or the
327
+ * column gap: `session-orchestrator-foo@x` is a different plugin.
328
+ * @param {string | undefined} listText
329
+ * @returns {Array<{id: string, marketplace: string | undefined, source: string | undefined}>}
330
+ */
331
+ function findCodexInstalls(listText) {
332
+ const row = new RegExp(`^${PLUGIN_NAME}(?:@(\\S+))?\\s+installed\\b`);
333
+ return (listText ?? '').split(/\r?\n/).flatMap((raw) => {
334
+ const line = raw.trim();
335
+ const m = row.exec(line);
336
+ if (!m) return [];
337
+ const cols = line.split(/\s{2,}/);
338
+ return [{ id: cols[0], marketplace: m[1], source: cols.length > 2 ? cols.at(-1) : undefined }];
339
+ });
340
+ }
341
+
342
+ /**
343
+ * The Codex step. `ctx` exists for tests (tmp clone, no PATH lookup);
344
+ * production passes nothing.
345
+ */
346
+ export function updateCodex({ run, log }, opts, ctx = {}) {
347
+ const { soRoot = SO_ROOT, hasCodex = () => commandOnPath('codex') } = ctx;
348
+ if (!hasCodex()) return { step: 'codex', status: 'skipped', detail: 'codex not on PATH' };
321
349
  const list = run('codex', ['plugin', 'list'], { mutates: false });
322
350
  if (!list.ok) return { step: 'codex', status: 'failed', detail: list.detail };
323
- if (!new RegExp(`^${PLUGIN_ID}\\s+installed`, 'm').test(list.stdout)) {
351
+ const installs = findCodexInstalls(list.stdout);
352
+ if (installs.length === 0) {
324
353
  return { step: 'codex', status: 'skipped', detail: 'plugin not installed in Codex (install: node scripts/codex-install.mjs)' };
325
354
  }
355
+ // `@local` comes from the personal marketplace
356
+ // (~/.agents/plugins/marketplace.json), whose directory source Codex caches
357
+ // whole, a checkout's .env.local included (#1518). codex-install would remove
358
+ // `@local` — a re-registration the operator owns — so this step only names
359
+ // the cleanup (same steps as codex-install's personal-marketplace hint),
360
+ // wherever the source points: run from a worktree it is the main checkout,
361
+ // not soRoot, and a source match would let codex-install remove it (#1608).
362
+ const local = installs.find((i) => i.marketplace === 'local');
363
+ if (local) {
364
+ const link = local.source ? `the link '${local.source}'` : 'its source link';
365
+ return {
366
+ step: 'codex',
367
+ status: 'failed',
368
+ detail: `${local.id} is installed from the personal marketplace${local.source ? ` via '${local.source}'` : ''} `
369
+ + `— its Codex cache copies the whole directory, a checkout's .env.local included (#1518). Clean up once: `
370
+ + `(1) node scripts/codex-install.mjs (installs ${PLUGIN_ID} from the packed copy, then removes ${local.id}); `
371
+ + `(2) remove that entry from ~/.agents/plugins/marketplace.json; (3) delete ${link} if it is a symlink; `
372
+ + `(4) after restarting Codex, delete the old cache folders under ~/.codex/plugins/cache/local/.`,
373
+ };
374
+ }
326
375
  log('codex: re-running codex-install');
327
- const r = run(process.execPath, [path.join(SO_ROOT, 'scripts', 'codex-install.mjs'), '--json']);
376
+ const r = run(process.execPath, [path.join(soRoot, 'scripts', 'codex-install.mjs'), '--json']);
328
377
  if (!r.ok) return { step: 'codex', status: 'failed', detail: r.detail };
329
378
  if (opts.dryRun) return { step: 'codex', status: 'ok', detail: 'dry-run' };
330
379
  try {
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-commit-files.mjs — which files did one session commit, in which of
4
+ * its own work trees (#1659).
5
+ *
6
+ * Read-only CLI over scripts/lib/session-work-trees.mjs: the session's
7
+ * `(tree, start_ref)` pairs come from the `orchestrator.session.work_tree`
8
+ * events in the default events ledger, and every tree is diffed INSIDE ITSELF
9
+ * against its own start ref (`git -C <tree> diff --name-only <ref>..HEAD`).
10
+ * Never a diff across two trees, never a write.
11
+ *
12
+ * Usage:
13
+ * node scripts/session-commit-files.mjs [--session-id <raw-id>] [--json]
14
+ *
15
+ * Exit codes: 0 success (also with zero trees), 2 usage error (unknown flag,
16
+ * missing session id). Deviates from cli-design.md's `1` for input errors by
17
+ * coordinator contract (#1659); documented in `--help`.
18
+ */
19
+
20
+ import { parseArgs } from 'node:util';
21
+
22
+ import { isMainModule } from './lib/is-main-module.mjs';
23
+ import { readSessionWorkTrees, sessionCommitFiles } from './lib/session-work-trees.mjs';
24
+
25
+ const USAGE = `Usage: node scripts/session-commit-files.mjs [--session-id <raw-id>] [--json]
26
+
27
+ List the files one session committed, per own work tree (#1659).
28
+
29
+ Reads the session's (tree, start_ref) pairs from the
30
+ orchestrator.session.work_tree events of the default events ledger and runs
31
+ 'git -C <tree> diff --name-only <start_ref>..HEAD' inside each tree only.
32
+ Read-only: writes nothing.
33
+
34
+ Options:
35
+ --session-id <id> raw session id (default: $CLAUDE_CODE_SESSION_ID)
36
+ --json print { trees, perTree, skipped, union, malformed, invalid,
37
+ complete[, error] } as JSON (complete: null = no ledger found)
38
+ -h, --help show this help
39
+
40
+ Default output (stdout): one line per tree '<tree>\\t<start_ref>\\t<n> files',
41
+ a blank line, then the union of files, one per line. Skipped trees and notes
42
+ go to stderr.
43
+
44
+ Exit codes:
45
+ 0 success (also when the session recorded no trees)
46
+ 2 usage error (unknown option, no session id)
47
+
48
+ Example:
49
+ node scripts/session-commit-files.mjs --session-id "$CLAUDE_CODE_SESSION_ID" --json`;
50
+
51
+ /**
52
+ * Run the CLI.
53
+ *
54
+ * @param {string[]} argv — arguments after the script path.
55
+ * @param {{ env?: NodeJS.ProcessEnv,
56
+ * stdout?: (s: string) => void, stderr?: (s: string) => void }} [io]
57
+ * @returns {number} exit code
58
+ */
59
+ export function main(argv, {
60
+ env = process.env,
61
+ stdout = (s) => process.stdout.write(s),
62
+ stderr = (s) => process.stderr.write(s),
63
+ } = {}) {
64
+ let values;
65
+ try {
66
+ ({ values } = parseArgs({
67
+ args: argv,
68
+ options: {
69
+ 'session-id': { type: 'string' },
70
+ json: { type: 'boolean', default: false },
71
+ help: { type: 'boolean', short: 'h', default: false },
72
+ },
73
+ strict: true,
74
+ allowPositionals: false,
75
+ }));
76
+ } catch (err) {
77
+ stderr(`session-commit-files: ${err.message}\n${USAGE}\n`);
78
+ return 2;
79
+ }
80
+ if (values.help) {
81
+ stdout(`${USAGE}\n`);
82
+ return 0;
83
+ }
84
+
85
+ // Process-local identity only — never a repo-global file (#1659).
86
+ const sessionId = (values['session-id'] ?? env.CLAUDE_CODE_SESSION_ID ?? '').trim();
87
+ if (sessionId === '') {
88
+ stderr('session-commit-files: no session id (pass --session-id or set CLAUDE_CODE_SESSION_ID)\n');
89
+ return 2;
90
+ }
91
+
92
+ const read = readSessionWorkTrees({ sessionId });
93
+ const files = sessionCommitFiles({ trees: read.trees });
94
+
95
+ // Notes go to stderr in BOTH modes, so `--json` never hides that the
96
+ // answer is partial or that no ledger exists (#1659).
97
+ if (read.error) stderr(`session-commit-files: ledger read failed: ${read.error}\n`);
98
+ if (read.complete === null && !read.error) stderr('session-commit-files: no events ledger found\n');
99
+ if (read.malformed > 0) stderr(`session-commit-files: ${read.malformed} unreadable ledger line(s)\n`);
100
+ if (read.invalid > 0) {
101
+ stderr(`session-commit-files: ignored ${read.invalid} own work_tree event(s) without a valid tree/start_ref\n`);
102
+ }
103
+ for (const s of files.skipped) {
104
+ stderr(`session-commit-files: skipped ${s.tree} (${s.start_ref}): ${s.reason}\n`);
105
+ }
106
+
107
+ if (values.json) {
108
+ stdout(`${JSON.stringify({
109
+ trees: read.trees,
110
+ perTree: files.perTree,
111
+ skipped: files.skipped,
112
+ union: files.union,
113
+ malformed: read.malformed,
114
+ invalid: read.invalid,
115
+ complete: read.complete,
116
+ ...(read.error ? { error: read.error } : {}),
117
+ }, null, 2)}\n`);
118
+ return 0;
119
+ }
120
+
121
+ if (read.trees.length === 0) {
122
+ if (read.complete !== null) stderr(`session-commit-files: no work trees recorded for session ${sessionId}\n`);
123
+ return 0;
124
+ }
125
+ if (files.perTree.length === 0) return 0; // every tree skipped — reported on stderr above
126
+ const lines = files.perTree.map((t) => `${t.tree}\t${t.start_ref}\t${t.files.length} files`);
127
+ stdout(`${lines.join('\n')}\n\n${files.union.map((f) => `${f}\n`).join('')}`);
128
+ return 0;
129
+ }
130
+
131
+ if (isMainModule(import.meta.url)) {
132
+ process.exitCode = main(process.argv.slice(2));
133
+ }