session-orchestrator 3.22.0 → 3.24.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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -0,0 +1,277 @@
1
+ /**
2
+ * session-transition.mjs — the PROCESS-BOUNDARY teardown of a source repo root.
3
+ *
4
+ * Issue #1069 ("EnterWorktree/CwdChanged als eindeutigen Ein-Root-Übergang
5
+ * modellieren"), operator decision 2026-08-28: **Prozessgrenze**, not live
6
+ * migration. The source session ENDS regularly in the old root; a NEW session
7
+ * with its own identity starts in the destination worktree. Nothing is
8
+ * transferred — no lock hand-off, no registry rewrite, no proof migration.
9
+ *
10
+ * The gap this closes: session-start Phase 0.5 Worktree-Promotion calls
11
+ * `enterWorktree()` and then "exits Phase 0 immediately". It released nothing
12
+ * on the OLD root (measured 2026-08-28: zero `deregisterSelf` / `release(`
13
+ * call sites across `skills/session-start/SKILL.md` and
14
+ * `skills/_shared/parallel-aware-auq.md`). The abandoned registry entry keeps
15
+ * reading FRESH to `detectPeers()` for `freshnessMin=15` minutes and is only
16
+ * removed by `sweepZombies()` after `thresholdMin=60` — so the promoted-away
17
+ * root advertises a PHANTOM peer for up to an hour, and the abandoned
18
+ * `session.lock` blocks/skews every exclusivity decision in that root until
19
+ * its TTL lapses.
20
+ *
21
+ * Ownership rules are NOT re-derived here. The lock half is the same shape
22
+ * `hooks/on-session-end.mjs` uses: `readLockDetailed()` to distinguish an
23
+ * absent lock from an unparseable one, an exact raw `lock.session_id ===
24
+ * sessionId` compare BEFORE any proof is loaded, then `loadOwnerProof()`
25
+ * handed to `release()` as the second identity factor (#987/#989).
26
+ *
27
+ * ## Deliberate deltas vs `hooks/on-session-end.mjs` (its release block)
28
+ *
29
+ * The two teardowns share the ownership gate and now the
30
+ * `orchestrator.session.lock.released` breadcrumb, and diverge in four places
31
+ * ON PURPOSE. Each divergence is a narrower authority, never an oversight:
32
+ *
33
+ * 1. **No reconciliation on a raw-ID mismatch.** SessionEnd calls
34
+ * `attemptLockReconciliation()`, which may reap a provably-dead foreign
35
+ * lease. A worktree promotion is not a lifecycle end for the foreign
36
+ * session and carries no mandate over its lease: this function returns
37
+ * `lock-session-mismatch:<owner>` and touches nothing.
38
+ * 2. **No owner-proof unlink after a successful release.** SessionEnd removes
39
+ * the genesis proof as hygiene because the session is ending. Here the
40
+ * session continues in the DESTINATION root; the source proof is
41
+ * self-invalidating (its millisecond `started_at` can never match a future
42
+ * lock) and deleting it would be a write outside the teardown's remit.
43
+ * 3. **No `orchestrator.session.lock.read_anomaly` breadcrumb.** An
44
+ * unparseable lock aborts this teardown outright and is reported in the
45
+ * RETURN value (`ok: false`, `reason: 'lock-unreadable'|'lock-corrupt'`),
46
+ * which the caller must surface as a WARN. SessionEnd has no return channel
47
+ * to its caller, so there the event is the only place the anomaly can land.
48
+ * 4. **No `orchestrator.session.lock.release_failed` breadcrumb.** Same
49
+ * reason as (3): a failed release surfaces as `ok: false` plus
50
+ * `reason: 'lock-<release-reason>'` to a caller that is required to WARN on
51
+ * it, so the stream is not the only witness.
52
+ *
53
+ * @module scripts/lib/session-transition
54
+ */
55
+
56
+ import path from 'node:path';
57
+
58
+ import { deregisterSelf, readRegistry, repoPathHash } from './session-registry.mjs';
59
+ import { readLockDetailed, loadOwnerProof, release } from './session-lock.mjs';
60
+ import { emitEvent } from './events.mjs';
61
+
62
+ /** Event name emitted once per completed source-root departure. */
63
+ export const ROOT_LEFT_EVENT = 'orchestrator.session.root_left';
64
+
65
+ /** Shared lock-release breadcrumb — same event the other two `release()` call sites emit. */
66
+ export const LOCK_RELEASED_EVENT = 'orchestrator.session.lock.released';
67
+
68
+ /**
69
+ * Release this session's claim on `repoRoot` because the session is leaving it
70
+ * for good. Ordering is load-bearing and fixed:
71
+ *
72
+ * 1. `readLockDetailed()` — OWNERSHIP IS ESTABLISHED BEFORE ANYTHING IS
73
+ * REMOVED. A lock this session does not own, or one that cannot be parsed,
74
+ * aborts the teardown with `ok: false` and zero side effects. (This used to
75
+ * run second, after the registry entry had already been deleted: a caller
76
+ * passing the wrong `sessionId` destroyed a live registry entry and was
77
+ * told `ok: true` — the registry half of the very phantom-peer state the
78
+ * function exists to prevent.)
79
+ * 2. `deregisterSelf(sessionId)` — the host-wide registry entry. The registry
80
+ * is one file per harness session id, so this removes the session's ONLY
81
+ * entry; the destination worktree's own SessionStart registers itself
82
+ * afresh under its own identity (that is what "process boundary" means).
83
+ * Gated a second time on the entry's `repo_path_hash`: an entry that
84
+ * describes a DIFFERENT root is not this root's claim, and deleting it
85
+ * would silently unregister a session living somewhere else.
86
+ * 3. `release()` of the old root's `session.lock`, gated exactly as
87
+ * `hooks/on-session-end.mjs` gates it, followed by the shared
88
+ * `orchestrator.session.lock.released` breadcrumb (`caller:
89
+ * 'session-transition'`) so this third `release()` call site is visible on
90
+ * the same stream as the other two.
91
+ * 4. `emitEvent(ROOT_LEFT_EVENT)` into the OLD root's events.jsonl — the
92
+ * breadcrumb that says which root was left and why. Emitted even when a
93
+ * step above found nothing: "nothing to release" is itself a fact worth
94
+ * recording, and a departure that left no trace is exactly the
95
+ * indistinguishable-from-a-crash state #1069 was filed about. NOT emitted
96
+ * on an aborted teardown (foreign lock, unparseable lock, invalid args):
97
+ * the event asserts that a root was LEFT, and on those branches nothing
98
+ * was — writing it would put a fiction in a stream that, unlike the
99
+ * registry, may belong to the session that actually owns this root.
100
+ *
101
+ * The caller must invoke this BEFORE the destination worktree's own Phase 1.2
102
+ * `acquire()` runs — never both roots owning a live lock/registry entry at
103
+ * once, which is the double-live-UUID state the issue's acceptance criteria
104
+ * forbid.
105
+ *
106
+ * NEVER THROWS. Every failure is reported in the returned structure, because
107
+ * the sole caller is a promotion path that has already created the destination
108
+ * worktree: a thrown error there would abort the transition halfway, leaving
109
+ * precisely the two-live-roots state this function exists to prevent.
110
+ *
111
+ * @param {object} args
112
+ * @param {string} args.repoRoot — absolute path of the root being LEFT.
113
+ * @param {string} args.sessionId — the raw (physical) session id that owns the
114
+ * registry entry and the lock in `repoRoot`. NOT the semantic id.
115
+ * @param {string|null} [args.semanticSessionId] — semantic id, when known.
116
+ * Omitted from the event payload when null/absent rather than emitted as
117
+ * `null` (same honest-encoding rule as `sessionAttribution()` in events.mjs).
118
+ * @param {string} [args.reason='unspecified'] — why the root is being left,
119
+ * e.g. `'worktree-promotion'`. Recorded verbatim in the event payload.
120
+ * Defaults to `'unspecified'` rather than guessing `'worktree-promotion'`:
121
+ * a mislabelled reason is worse than an admittedly unknown one.
122
+ * @param {object} [deps] — test seams (same shape as `foreign-dispatch.mjs`).
123
+ * @param {Function} [deps.emitFn] — event-emitter seam; defaults to `emitEvent`.
124
+ * @returns {Promise<{
125
+ * ok: boolean,
126
+ * steps: { deregistered: boolean, released: boolean, emitted: boolean },
127
+ * reason?: string
128
+ * }>}
129
+ * `steps` are literal outcomes: `deregistered` = a registry file was removed,
130
+ * `released` = the lock file was deleted, `emitted` = the `root_left` event
131
+ * was appended.
132
+ *
133
+ * `ok: false` means the departure is NOT provably complete — invalid input, a
134
+ * lock owned by someone else, an unparseable lock, a registry entry pinned to
135
+ * a different root, a registry unlink that failed, a contradictory owner
136
+ * proof, or a filesystem error. Callers WARN on it and continue; see the
137
+ * `enterWorktree: leaveSourceRoot: <reason>` contract in
138
+ * `skills/_shared/parallel-aware-auq.md`.
139
+ *
140
+ * `reason` is present whenever something other than a full clean teardown
141
+ * happened; `'already-gone'` is the idempotent-rerun case (nothing left to
142
+ * remove), which is `ok: true`.
143
+ */
144
+ export async function leaveSourceRoot({
145
+ repoRoot,
146
+ sessionId,
147
+ semanticSessionId = null,
148
+ reason = 'unspecified',
149
+ } = {}, deps = {}) {
150
+ const emit = deps.emitFn ?? emitEvent;
151
+ const steps = { deregistered: false, released: false, emitted: false };
152
+
153
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0
154
+ || typeof sessionId !== 'string' || sessionId.length === 0) {
155
+ // Nothing is attempted on unusable input — in particular no event, since
156
+ // an event without a resolvable root has nowhere honest to be written.
157
+ return { ok: false, steps, reason: 'invalid-args' };
158
+ }
159
+
160
+ // -- Step 1: ownership, BEFORE any removal --------------------------------
161
+ // Mirrors hooks/on-session-end.mjs's release block: discriminate the read
162
+ // first, compare the RAW session_id before touching the proof, and only then
163
+ // hand the proof to release() as a second factor. An 'absent' lock is the
164
+ // idempotent-rerun / never-acquired case and does NOT block the teardown.
165
+ const lockDetail = readLockDetailed({ repoRoot });
166
+
167
+ if (lockDetail.status === 'unreadable' || lockDetail.status === 'corrupt') {
168
+ // Fail-closed and VISIBLE (issue #1069 AC: "fehlende oder widersprüchliche
169
+ // Proofs degradieren sichtbar und fail-closed"). A lock we cannot parse is
170
+ // neither released nor proven foreign — ownership stays indeterminate, so
171
+ // nothing is removed and nothing is claimed.
172
+ return { ok: false, steps, reason: `lock-${lockDetail.status}` };
173
+ }
174
+
175
+ if (lockDetail.status === 'ok' && lockDetail.lock.session_id !== sessionId) {
176
+ // A different session owns this root's lock, so this root is not ours to
177
+ // tear down — not the lock, and not the registry entry either. Aborting
178
+ // with `ok: false` (rather than the pre-#1069-review `ok: true`) is the
179
+ // point: a caller that passed the wrong id has NOT departed anything, and
180
+ // reporting success let that mistake through silently.
181
+ return { ok: false, steps, reason: `lock-session-mismatch:${lockDetail.lock.session_id}` };
182
+ }
183
+
184
+ let ok = true;
185
+ let outcome = null;
186
+
187
+ // -- Step 2: registry (host-wide) -----------------------------------------
188
+ // Second ownership factor, on the registry's own terms: the entry records the
189
+ // root it belongs to as a hash. When it names a DIFFERENT root, the entry is
190
+ // some other checkout's live claim and unlinking it would unregister a
191
+ // session that is still running there. Legacy v1 entries carry no hash — the
192
+ // gate is skipped rather than fail-closed, because rejecting them would make
193
+ // this teardown a no-op against exactly the old entries most likely to be
194
+ // stale (same back-compat rule as `_validEntry`'s optional `mode`).
195
+ let registryGateOk = true;
196
+ try {
197
+ const entry = (await readRegistry()).find((e) => e.session_id === sessionId);
198
+ if (entry && typeof entry.repo_path_hash === 'string' && entry.repo_path_hash.length > 0
199
+ && entry.repo_path_hash !== repoPathHash(repoRoot)) {
200
+ registryGateOk = false;
201
+ ok = false;
202
+ outcome ??= 'registry-root-mismatch';
203
+ }
204
+ } catch { /* an unreadable registry is handled by deregisterSelf below */ }
205
+
206
+ if (registryGateOk) {
207
+ try {
208
+ steps.deregistered = await deregisterSelf(sessionId);
209
+ } catch (err) {
210
+ ok = false;
211
+ outcome ??= `deregister-failed:${err?.message ?? 'unknown'}`;
212
+ }
213
+ }
214
+
215
+ // -- Step 3: lock (this root only) ----------------------------------------
216
+ if (lockDetail.status === 'ok') {
217
+ const proof = loadOwnerProof({ repoRoot });
218
+ // `proof` is null whenever ownership cannot be proven; release() treats
219
+ // null as absent (#989) and degrades to the session_id-only path, so it
220
+ // is passed through unguarded — the spread-guard call sites needed
221
+ // before that fix are exactly what the API contract removed.
222
+ const result = release({ sessionId, repoRoot, proof });
223
+ steps.released = result.ok === true && result.deleted === true;
224
+ // 'no-lock' = the lock vanished between the read above and the release —
225
+ // benign (idempotent rerun / concurrent SessionEnd), and forensically the
226
+ // interesting half: someone else removed a lock we still held.
227
+ const alreadyGone = result.ok === true && result.reason === 'no-lock';
228
+ if (!steps.released) {
229
+ if (!alreadyGone) ok = false;
230
+ outcome ??= `lock-${result.reason ?? 'not-deleted'}`;
231
+ }
232
+ if (steps.released || alreadyGone) {
233
+ // ARCH-MED-1 — this is the THIRD `release()` call site in the repo and
234
+ // was the only one silent on `orchestrator.session.lock.*`. Emitted at
235
+ // the call site, never inside release(): session-lock.mjs carries no
236
+ // dependency on events.mjs, and release() is synchronous.
237
+ try {
238
+ await emit(LOCK_RELEASED_EVENT, {
239
+ session_id: sessionId,
240
+ ...(semanticSessionId ? { semantic_session_id: semanticSessionId } : {}),
241
+ caller: 'session-transition',
242
+ outcome: steps.released ? 'deleted' : 'already-gone',
243
+ verified: result.verified === true,
244
+ }, { repoRoot });
245
+ } catch { /* observability is best-effort — the return value still tells */ }
246
+ }
247
+ }
248
+
249
+ if (outcome === null && !steps.deregistered && !steps.released) {
250
+ outcome = 'already-gone';
251
+ }
252
+
253
+ // -- Step 4: breadcrumb ----------------------------------------------------
254
+ try {
255
+ await emit(ROOT_LEFT_EVENT, {
256
+ session_id: sessionId,
257
+ ...(semanticSessionId ? { semantic_session_id: semanticSessionId } : {}),
258
+ // NEVER the absolute path. emitEvent forwards the whole payload to the
259
+ // optional Clank webhook with no redaction, and an absolute repo root on
260
+ // this host is `/Users/<operator>/…`. The hash is the SAME function the
261
+ // session registry keys its entries by (`repo_path_hash`), so a consumer
262
+ // can still join a departure to the registry entry it removed; the
263
+ // basename matches the registry's `repo_name`. Same rule as the
264
+ // `board_written` / `mirror_completed` payloads and
265
+ // `relativeWorktreePath` in worktree-pipeline.mjs.
266
+ from_root_hash: repoPathHash(repoRoot),
267
+ from_root_basename: path.basename(path.resolve(repoRoot)),
268
+ reason,
269
+ }, { repoRoot });
270
+ steps.emitted = true;
271
+ } catch (err) {
272
+ ok = false;
273
+ outcome ??= `emit-failed:${err?.message ?? 'unknown'}`;
274
+ }
275
+
276
+ return outcome === null ? { ok, steps } : { ok, steps, reason: outcome };
277
+ }