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
@@ -3,7 +3,8 @@
3
3
  * migrate-vault-paths.mjs — one-shot cross-repo migration for username-drift in vault/project paths.
4
4
  *
5
5
  * Rewrites a literal "from" username path-segment to a "to" segment across markdown
6
- * files under ~/Projects/. Only the username path segment changes; the trailing
6
+ * files under the projects root (getConfinementRoot(): SO_PROJECTS_ROOT >
7
+ * owner.yaml paths.projects-root > ~/Projects). Only the username path segment changes; the trailing
7
8
  * path is preserved.
8
9
  *
9
10
  * Typical use: a host username change (`/Users/oldname/` → `/Users/newname/`) leaves
@@ -23,7 +24,7 @@
23
24
  * Target repo resolution (in priority order):
24
25
  * 1. --repos <comma,list> (explicit CLI override)
25
26
  * 2. audited-repos: [...] in the same vault-migration-rules.yaml
26
- * 3. <none, no --repos> → scan only the auto-discover sweep under ~/Projects/**.
27
+ * 3. <none, no --repos> → scan only the auto-discover sweep under <projects-root>/**.
27
28
  *
28
29
  * Flags:
29
30
  * --from SEG Literal source segment (e.g. '/Users/oldname/'). Required if no config entry.
@@ -61,13 +62,13 @@ import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
61
62
  import { isMainModule } from './lib/is-main-module.mjs';
62
63
  import { atomicWriteText, envelopeToError } from './lib/io.mjs';
63
64
  import { loadHostPaths, resolveHostPath } from './lib/config/host-paths.mjs';
65
+ import { getConfinementRoot } from './lib/config/cross-repo.mjs';
64
66
 
65
67
  // ---------------------------------------------------------------------------
66
68
  // Constants
67
69
  // ---------------------------------------------------------------------------
68
70
 
69
71
  const HOME = os.homedir();
70
- const PROJECTS_ROOT = path.join(HOME, 'Projects');
71
72
 
72
73
  // Set at startup from CLI args or config. Helpers read these; main() resolves them.
73
74
  let OLD_SEGMENT = null;
@@ -221,7 +222,8 @@ function printHelp() {
221
222
  `Usage: migrate-vault-paths.mjs --from <old/> --to <new/> [--dry-run|--apply] [--repos <comma,list>] [--json] [--help]
222
223
 
223
224
  Rewrites a literal "from" path segment to a "to" segment across markdown files
224
- under ~/Projects/, skipping tests/, fixtures/, .git/, vault-backups, and
225
+ under the projects root (SO_PROJECTS_ROOT / owner.yaml paths.projects-root,
226
+ default ~/Projects), skipping tests/, fixtures/, .git/, vault-backups, and
225
227
  historical contexts (decisions.md, history/, archive*).
226
228
 
227
229
  Both --from and --to are required unless username-rewrites[0] is set in
@@ -236,7 +238,7 @@ Options:
236
238
  --apply Write fixes in-place (atomic: tmp + rename)
237
239
  --repos LIST Comma-separated repo names or paths to scan; falls back
238
240
  to audited-repos: [...] in vault-migration-rules.yaml,
239
- then to ~/Projects/** auto-discovery sweep
241
+ then to <projects-root>/** auto-discovery sweep
240
242
  --json Emit JSONL records on stdout (one per hit)
241
243
  --help, -h Show this help
242
244
 
@@ -249,8 +251,21 @@ Exit codes: 0 success 1 input error 2 I/O error
249
251
  // Repo discovery
250
252
  // ---------------------------------------------------------------------------
251
253
 
254
+ let _projectsRoot;
255
+
256
+ /**
257
+ * Projects root for this run, resolved once (#1622): getConfinementRoot() reads
258
+ * owner.yaml and WARNs on an invalid SO_PROJECTS_ROOT, so per-repo calls repeated both.
259
+ * @returns {string}
260
+ */
261
+ function projectsRoot() {
262
+ _projectsRoot ??= getConfinementRoot();
263
+ return _projectsRoot;
264
+ }
265
+
252
266
  /**
253
- * Resolve a repo name to an absolute path by searching ~/Projects/**.
267
+ * Resolve a repo name to an absolute path by searching <projects-root>/**
268
+ * (getConfinementRoot(), #1567).
254
269
  * If the input is already an absolute path that exists, returns it as-is.
255
270
  * Returns null when no match is found.
256
271
  */
@@ -258,10 +273,10 @@ function resolveRepoPath(nameOrPath) {
258
273
  if (path.isAbsolute(nameOrPath)) {
259
274
  return existsSync(nameOrPath) ? nameOrPath : null;
260
275
  }
261
- // Search up to 4 levels deep under ~/Projects/
276
+ // Search up to 4 levels deep under the projects root
262
277
  const result = spawnSync(
263
278
  'find',
264
- [PROJECTS_ROOT, '-maxdepth', '4', '-type', 'd', '-name', nameOrPath],
279
+ [projectsRoot(), '-maxdepth', '4', '-type', 'd', '-name', nameOrPath],
265
280
  { encoding: 'utf8' },
266
281
  );
267
282
  if (result.status !== 0) return null;
@@ -358,14 +373,15 @@ function findMissingSegmentFiles(roots) {
358
373
  }
359
374
 
360
375
  /**
361
- * Auto-discover ALL repos under ~/Projects/** that contain the OLD_SEGMENT
376
+ * Auto-discover ALL repos under <projects-root>/** that contain the OLD_SEGMENT
362
377
  * literal OR a missing-segment `vault-dir:` drift. Used in tandem with the
363
378
  * default-target list to find drift we don't already know about.
364
379
  */
365
380
  function discoverAdditionalFiles() {
366
- if (!existsSync(PROJECTS_ROOT)) return [];
367
- const set = new Set(findCandidateFiles([PROJECTS_ROOT]));
368
- for (const f of findMissingSegmentFiles([PROJECTS_ROOT])) set.add(f);
381
+ const root = projectsRoot();
382
+ if (!existsSync(root)) return [];
383
+ const set = new Set(findCandidateFiles([root]));
384
+ for (const f of findMissingSegmentFiles([root])) set.add(f);
369
385
  return [...set];
370
386
  }
371
387
 
@@ -599,7 +615,7 @@ async function main() {
599
615
  let candidateFiles = [...candidateSet];
600
616
 
601
617
  // If NO explicit repo list was given (neither --repos nor config.audited-repos),
602
- // expand scan to discover anything else under ~/Projects/**.
618
+ // expand scan to discover anything else under <projects-root>/**.
603
619
  if (!explicitRepos) {
604
620
  const additional = discoverAdditionalFiles();
605
621
  const set = new Set(candidateFiles);
@@ -8,11 +8,12 @@
8
8
  *
9
9
  * Output: Single JSON object to stdout with ALL config fields (defaults applied).
10
10
  * `--json` is an explicit NO-OP alias for that default — accepted because
11
- * `docs/session-config-template.md` (lines 13 and 965) documents
12
- * `node scripts/parse-config.mjs --json` as THE validation command, and the
13
- * flag used to be swallowed by the positional-path slot and rejected with
14
- * `File not found: --json` (exit 1). `cli-design.md` § JSON-First Output also
15
- * requires every CLI to accept `--json`.
11
+ * `docs/session-config-template.md` ("How to use" and "Validation &
12
+ * Bootstrap") documents `node scripts/parse-config.mjs --json` as THE
13
+ * validation command, and the flag used to be swallowed by the
14
+ * positional-path slot and rejected with `File not found: --json` (exit 1).
15
+ * `cli-design.md` § JSON-First Output also requires every CLI to accept
16
+ * `--json`.
16
17
  * Exit codes: 0 success, 1 error (message to stderr)
17
18
  *
18
19
  * Unparsable lines inside `## Session Config` (#1097) are reported per line on
@@ -20,6 +21,11 @@
20
21
  * `enforcement: strict`; `off` is silent. stdout is unchanged for any
21
22
  * well-formed block — a warning never alters the emitted JSON.
22
23
  *
24
+ * Unknown top-level keys (#1562) — a key the plugin's own
25
+ * `docs/session-config-template.md` does not document, i.e. a typo or a key
26
+ * from a newer plugin version — are reported per line on stderr under `warn`
27
+ * AND `strict` (a report, never a refusal); `off` is silent. stdout unchanged.
28
+ *
23
29
  * Environment:
24
30
  * SO_CONFIG_FILE — override filename (e.g. "AGENTS.md") resolved from project root
25
31
  * SO_SKIP_CONFIG_VALIDATION — set to "1" to bypass validate-config.mjs
@@ -30,7 +36,8 @@ import { spawnSync } from 'node:child_process';
30
36
  import { join, dirname, resolve, parse as parsePath } from 'node:path';
31
37
  import { fileURLToPath } from 'node:url';
32
38
  import { parseSessionConfig } from './lib/config.mjs';
33
- import { collectUnparsableLines } from './lib/config/section-extractor.mjs';
39
+ import { collectUnparsableLines, collectTopLevelKeyLines } from './lib/config/section-extractor.mjs';
40
+ import { knownTemplateKeys } from './lib/config/template-keys.mjs';
34
41
  import { findUnterminatedComment } from './lib/config/block-preprocess.mjs';
35
42
  import { ENFORCEMENT_VALUES } from './lib/config-schema.mjs';
36
43
 
@@ -189,15 +196,15 @@ try {
189
196
 
190
197
  const unparsableLines = collectUnparsableLines(content);
191
198
 
192
- if (unparsableLines.length > 0) {
193
- // Belt-and-braces, not a live branch: `parseSessionConfig` already REFUSES
194
- // an out-of-vocabulary enforcement value above (measured: `enforcement:
195
- // banana` exits 1 with "must be strict|warn|off" and never reaches here), so
196
- // `config.enforcement` is one of the three by the time this line runs. The
197
- // fallback exists for the direction that matters if that ever loosens — an
198
- // unknown value must degrade to `warn`, never arm the `strict` refusal.
199
- const enforcement = ENFORCEMENT_VALUES.has(config.enforcement) ? config.enforcement : 'warn';
199
+ // Belt-and-braces, not a live branch: `parseSessionConfig` already REFUSES
200
+ // an out-of-vocabulary enforcement value above (measured: `enforcement:
201
+ // banana` exits 1 with "must be strict|warn|off" and never reaches here), so
202
+ // `config.enforcement` is one of the three by the time this line runs. The
203
+ // fallback exists for the direction that matters if that ever loosens — an
204
+ // unknown value must degrade to `warn`, never arm the `strict` refusal.
205
+ const enforcement = ENFORCEMENT_VALUES.has(config.enforcement) ? config.enforcement : 'warn';
200
206
 
207
+ if (unparsableLines.length > 0) {
201
208
  if (enforcement !== 'off') {
202
209
  // Ceiling: 20 named lines. A whole prose section pasted into the block is
203
210
  // one defect, not 200, and the strict path below exits immediately after
@@ -230,6 +237,56 @@ if (unparsableLines.length > 0) {
230
237
  }
231
238
  }
232
239
 
240
+ // ---------------------------------------------------------------------------
241
+ // Unknown-key report (#1562) — a typo'd key is not a silent default either
242
+ // ---------------------------------------------------------------------------
243
+ //
244
+ // A well-formed line with a mis-spelled key (`persistance: true`) parses
245
+ // cleanly, so the gate above never sees it: the key lands in the KV map under a
246
+ // name nothing reads, and the real key keeps its default. The population of
247
+ // known keys is the plugin's OWN template (first + last `## Session Config`
248
+ // block, the population drift-check Check 6 reads) plus every top-level key
249
+ // `parseSessionConfig()` emits, so it moves with the plugin version, never
250
+ // with the repo.
251
+ //
252
+ // A report, never a refusal — even under `strict`: a key from a newer plugin
253
+ // version is not a defect of the repo. Three states stay apart on purpose:
254
+ // measured with findings (one line per key), measured clean (silence), and NOT
255
+ // measured (one line saying why) — "the check could not run" must never read
256
+ // like "no unknown keys".
257
+
258
+ if (enforcement !== 'off') {
259
+ const templatePath = join(SCRIPT_DIR, '..', 'docs', 'session-config-template.md');
260
+ let known = null;
261
+ let notRunReason = null;
262
+ try {
263
+ known = knownTemplateKeys(readFileSync(templatePath, 'utf8'));
264
+ if (!known) notRunReason = 'it has no distinct starter and opt-in `## Session Config` blocks';
265
+ // A key the parser itself emits is read, wherever the template documents it:
266
+ // `skill-evolution` and `dispatcher-autonomy` are parsed from the whole file
267
+ // (their parity-exempt template blocks sit outside `## Session Config`), so
268
+ // the same key inside the block is applied and must not read "no effect".
269
+ else for (const key of Object.keys(config)) known.add(key);
270
+ } catch (err) {
271
+ notRunReason = err.code === 'ENOENT' ? 'it was not found' : `it could not be read (${err.code ?? err.message})`;
272
+ }
273
+
274
+ if (notRunReason) {
275
+ process.stderr.write(
276
+ `⚠ parse-config.mjs: unknown Session Config key check did not run — ${templatePath}: ${notRunReason}\n`,
277
+ );
278
+ } else {
279
+ const configName = parsePath(configFile).base;
280
+ for (const { line, key } of collectTopLevelKeyLines(content)) {
281
+ if (known.has(key)) continue;
282
+ process.stderr.write(
283
+ `⚠ ${configName}:${line}: unknown Session Config key '${key}' — not in docs/session-config-template.md ` +
284
+ '(a typo, or a key from a newer plugin version); it has no effect\n',
285
+ );
286
+ }
287
+ }
288
+ }
289
+
233
290
  // jq -n produces pretty-printed JSON without a trailing newline — match that format
234
291
  const assembledJson = JSON.stringify(config, null, 2);
235
292
 
@@ -101,6 +101,7 @@ import { join, dirname } from 'node:path';
101
101
  import { fileURLToPath } from 'node:url';
102
102
 
103
103
  import { findProjectRoot } from './lib/common.mjs';
104
+ import { resolveLedgerPath } from './lib/ledger-path.mjs';
104
105
  import { eventsFilePath } from './lib/events.mjs';
105
106
  import { resolveStateArtifactPath } from './lib/state-md.mjs';
106
107
  import {
@@ -154,6 +155,11 @@ Options:
154
155
  --max-chars <n> Hard cap on the rendered index body (default: ${LEARNINGS_INDEX_MAX_CHARS}).
155
156
  --pool-size <n> Active entries pulled before ranking (default: ${CANDIDATE_POOL_SIZE}).
156
157
  --learnings <path> Learnings JSONL (default: .orchestrator/metrics/learnings.jsonl).
158
+ --evidence-candidates <path> Opt into a bounded local candidate JSON artifact.
159
+ Requires --source-repo and an exact non-empty --task-text.
160
+ Invalid/unavailable artifacts keep the baseline output;
161
+ provider status/counts go to stderr. No provider is run.
162
+ --source-repo <id> Explicit opaque repo ID bound to the candidate artifact.
157
163
  --rules-dir <path> Natively-delivered rule corpus (default: .claude/rules).
158
164
  Learnings already delivered as a rule file there are
159
165
  excluded from the index (#1019). A path that does not
@@ -208,6 +214,8 @@ try {
208
214
  'max-chars': { type: 'string' },
209
215
  'pool-size': { type: 'string' },
210
216
  learnings: { type: 'string' },
217
+ 'evidence-candidates': { type: 'string' },
218
+ 'source-repo': { type: 'string' },
211
219
  'rules-dir': { type: 'string' },
212
220
  'no-event': { type: 'boolean', default: false },
213
221
  json: { type: 'boolean', default: false },
@@ -219,6 +227,14 @@ try {
219
227
  }
220
228
 
221
229
  const opts = parsed.values;
230
+ if (opts['evidence-candidates'] !== undefined &&
231
+ (!opts['evidence-candidates'] || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(opts['source-repo'] ?? '') ||
232
+ !opts['task-text']?.trim() || opts['task-text'].length > 4000)) {
233
+ fail('--evidence-candidates requires a path, --source-repo ID and non-empty --task-text (at most 4000 characters)', 1);
234
+ }
235
+ if (opts['source-repo'] !== undefined && opts['evidence-candidates'] === undefined) {
236
+ fail('--source-repo requires --evidence-candidates', 1);
237
+ }
222
238
 
223
239
  /**
224
240
  * Parse a `--max-*` value: a non-negative integer. Anything else is a
@@ -249,7 +265,7 @@ const poolSize = parseCap(opts['pool-size'], CANDIDATE_POOL_SIZE, '--pool-size')
249
265
  const repoRoot = findProjectRoot(process.cwd());
250
266
  const learningsPath = opts.learnings
251
267
  ? opts.learnings
252
- : join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
268
+ : resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot); // #1722
253
269
  const eventsPath = eventsFilePath(repoRoot); // #1514 point 1: the repo's one ledger
254
270
  // #1019 — the natively-delivered rule corpus. Every `.claude/rules/*.md` reaches
255
271
  // a dispatched agent in FULL through Claude Code's own project-instruction
@@ -338,7 +354,11 @@ try {
338
354
  selection = await selectLearningsFromFile(
339
355
  learningsPath,
340
356
  { file_paths: scopePaths, text: taskText },
341
- { maxScoped, maxGlobal, maxChars, poolSize, rulesDir },
357
+ { maxScoped, maxGlobal, maxChars, poolSize, rulesDir,
358
+ ...(opts['evidence-candidates'] !== undefined ? {
359
+ evidenceCandidates: opts['evidence-candidates'], sourceRepo: opts['source-repo'],
360
+ } : {}),
361
+ },
342
362
  );
343
363
  } catch (err) {
344
364
  // `selectLearningsFromFile` is contractually total (contract point 1), so this
@@ -350,6 +370,9 @@ try {
350
370
 
351
371
  const selected = selection.entries;
352
372
  const scopeMatched = selection.scopeMatched;
373
+ if (selection.evidence) {
374
+ process.stderr.write(`[evidence-candidates] ${JSON.stringify(selection.evidence)}\n`);
375
+ }
353
376
 
354
377
  // ---------------------------------------------------------------------------
355
378
  // Render — the WRAPPER only; per-entry lines come from the selector
@@ -43,12 +43,15 @@
43
43
  */
44
44
 
45
45
  import { parseArgs } from 'node:util';
46
+ import { existsSync, realpathSync } from 'node:fs';
46
47
  import path from 'node:path';
47
48
 
48
- import { readLockDetailed, loadOwnerProof, release } from './lib/session-lock.mjs';
49
+ import { readLockDetailed, loadOwnerProof, release, writeReleaseMarker } from './lib/session-lock.mjs';
49
50
  import { LOCK_RELEASED_EVENT } from './lib/session-transition.mjs';
50
51
  import { emitEvent } from './lib/events.mjs';
51
52
  import { isMainModule } from './lib/is-main-module.mjs';
53
+ import { registrySessionRoot, resolveOwnLockRootFor } from './lib/own-lock-root.mjs';
54
+ import { dotGitAncestor } from './lib/platform.mjs';
52
55
 
53
56
  /**
54
57
  * `caller` value stamped into the breadcrumb, so this fourth release site stays
@@ -69,18 +72,31 @@ WHAT IT DOES
69
72
  3. Emits orchestrator.session.lock.released with the ids read BEFORE the
70
73
  delete, plus caller="${PHASE_CALLER}".
71
74
  4. RE-READS the lock path and exits 0 only when our lock is provably gone.
75
+ 5. Leaves .orchestrator/runtime/lock-released/<raw-id>.json, so a compaction
76
+ or resume of this session does not re-create the lock (#1651); the
77
+ session's next deliberate acquire() removes it.
72
78
 
73
79
  FLAGS
74
80
  --session-id <id> REQUIRED. The RAW (physical) session id that owns the lock
75
81
  — .orchestrator/session.lock "session_id", never STATE.md
76
82
  "session" and never the semantic id. A lock owned by a
77
83
  different id is NEVER released.
78
- --repo-root <path> Repo whose lock is released (default: cwd).
84
+ --repo-root <path> Repo whose lock is released. Default: the checkout whose
85
+ lock carries --session-id — the host registry's
86
+ session_root, the cwd's checkout, its main checkout, or a
87
+ registered linked worktree, in that order; with none of
88
+ them, the cwd's checkout (#1730). The JSON result names
89
+ the root (repo_root) and how it was found (root_source:
90
+ flag|registry|cwd|main-checkout|worktree|none).
79
91
  --json Machine-readable result on stdout.
80
92
  -h, --help Show this help and exit 0.
81
93
 
82
94
  OUTCOMES
83
95
  absent no lock file — nothing released, NO event, exit 0.
96
+
97
+ Without --repo-root the #1651 marker (absent and released alike) goes to the
98
+ lock's root, the cwd's checkout and the registry's session_root, existing
99
+ checkouts only; release_marker_roots lists each root with its result.
84
100
  deleted our lock was removed and re-read as gone.
85
101
  already-gone ownership matched but the lock had already vanished; the event
86
102
  is still written, because a lock removed by a third party while
@@ -137,6 +153,53 @@ function ourLockIsGone(repoRoot, sessionId) {
137
153
  return false;
138
154
  }
139
155
 
156
+ /** @param {string} p */
157
+ function canonical(p) {
158
+ try { return realpathSync.native(p); } catch { return path.resolve(p); }
159
+ }
160
+
161
+ /**
162
+ * Where the #1651 marker goes (#1730). A later compaction's SessionStart looks
163
+ * for it at `resolveSessionStartRoot`, which depends on THAT compaction's cwd —
164
+ * unknown here. So it goes to every root that reader can land on: the lock's
165
+ * root, the cwd's checkout and the registry's session_root. Only existing
166
+ * checkouts (a `.git` entry): a stale registry entry must not re-create the
167
+ * directory tree of a removed worktree. The marker is keyed on this session's
168
+ * own raw id, so an extra copy changes nothing for another session. With
169
+ * --repo-root the given root alone, as before.
170
+ *
171
+ * @param {{ root: string, source: string }} resolved
172
+ * @param {string} sessionId
173
+ * @returns {string[]}
174
+ */
175
+ function markerRootsFor(resolved, sessionId) {
176
+ if (resolved.source === 'flag') return [resolved.root];
177
+ const cwdTop = dotGitAncestor(process.cwd()) || path.resolve(process.cwd());
178
+ const extra = [cwdTop, registrySessionRoot(sessionId)]
179
+ .filter((r) => typeof r === 'string' && existsSync(path.join(r, '.git')));
180
+ return [...new Set([resolved.root, ...extra].map(canonical))];
181
+ }
182
+
183
+ /**
184
+ * Write the marker at each root; a failed copy is named on stderr and in the
185
+ * result, never hidden behind one that was written.
186
+ *
187
+ * @param {string[]} roots
188
+ * @param {string} sessionId
189
+ * @returns {Array<{ root: string, ok: boolean }>}
190
+ */
191
+ function writeMarkers(roots, sessionId) {
192
+ return roots.map((root) => {
193
+ const marker = writeReleaseMarker({ repoRoot: root, sessionId });
194
+ if (marker.ok !== true) {
195
+ process.stderr.write(
196
+ `release-session-lock: release marker not written at ${root} (${marker.reason}) — a compaction there may re-create the lock\n`,
197
+ );
198
+ }
199
+ return { root, ok: marker.ok === true };
200
+ });
201
+ }
202
+
140
203
  /**
141
204
  * CLI entry point.
142
205
  *
@@ -175,11 +238,20 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
175
238
  return 1;
176
239
  }
177
240
 
178
- const repoRoot = path.resolve(values['repo-root'] ?? process.cwd());
241
+ // #1730: `/close` runs this without --repo-root, and its cwd is often not the
242
+ // checkout holding the lock — a worktree session's lock sits in the main
243
+ // checkout, or the shell cwd reset to the main checkout while the lock is in
244
+ // a worktree. Reading at the cwd answered `absent` with exit 0 and the lock
245
+ // stayed. An explicit --repo-root is taken as given.
246
+ const resolved = values['repo-root'] !== undefined
247
+ ? { root: path.resolve(values['repo-root']), source: 'flag' }
248
+ : resolveOwnLockRootFor({ sessionId, cwd: process.cwd() });
249
+ const repoRoot = resolved.root;
179
250
 
180
251
  const emitResult = async (result) => {
181
- if (values.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
182
- else printHuman(result);
252
+ const located = { ...result, repo_root: repoRoot, root_source: resolved.source };
253
+ if (values.json) process.stdout.write(JSON.stringify(located, null, 2) + '\n');
254
+ else printHuman(located);
183
255
  };
184
256
 
185
257
  // -- Step 1: read the lock, and capture the ids BEFORE anything is deleted --
@@ -188,8 +260,14 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
188
260
  if (lockDetail.status === 'absent') {
189
261
  // Nothing was released, so nothing terminal happened: NO event. Writing one
190
262
  // here would put a fictional lifecycle end into the stream — the idempotent
191
- // re-run case is not a release.
192
- const result = { ok: true, outcome: 'absent', verified: true, session_id: sessionId, event_emitted: false };
263
+ // re-run case is not a release. The #1651 marker IS written: a session that
264
+ // closes without a lock (bootstrap fs-error, or an earlier release) must not
265
+ // get one re-created by a later compaction either (roots: markerRootsFor).
266
+ const markers = writeMarkers(markerRootsFor(resolved, sessionId), sessionId);
267
+ const result = {
268
+ ok: true, outcome: 'absent', verified: true, session_id: sessionId, event_emitted: false,
269
+ release_marker: markers.every((m) => m.ok), release_marker_roots: markers,
270
+ };
193
271
  await emitResult(result);
194
272
  return 0;
195
273
  }
@@ -249,6 +327,13 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
249
327
  const verified = ourLockIsGone(repoRoot, sessionId);
250
328
  const outcome = alreadyGone ? 'already-gone' : 'deleted';
251
329
 
330
+ // #1651: leave "released on purpose" behind, so the SessionStart bootstrap
331
+ // does not re-create this lock after a compaction or resume of the same raw
332
+ // session (a headless run may hold the slot by then). Best-effort: a failed
333
+ // marker write is reported, but the release itself stands.
334
+ const markers = verified ? writeMarkers(markerRootsFor(resolved, sessionId), sessionId) : [];
335
+ const markerWritten = verified && markers.every((m) => m.ok);
336
+
252
337
  // -- Step 4: the terminal breadcrumb --------------------------------------
253
338
  // The ids are passed EXPLICITLY: the lock is gone by now, so emitEvent's
254
339
  // lock-derived correlation would find nothing to attribute this record to.
@@ -275,6 +360,8 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
275
360
  ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
276
361
  event_emitted: eventEmitted,
277
362
  ...(emitError !== null ? { event_error: emitError } : {}),
363
+ release_marker: markerWritten,
364
+ release_marker_roots: markers,
278
365
  };
279
366
  await emitResult(result);
280
367