session-orchestrator 3.21.0 → 3.23.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 (345) 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 +11 -2
  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/040-discovery.mdc +6 -6
  38. package/.cursor/rules/050-plan.mdc +10 -10
  39. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  40. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  41. package/.cursor/skills/architecture/SKILL.md +13 -0
  42. package/.cursor/skills/autopilot/SKILL.md +12 -0
  43. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  44. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  45. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  46. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  47. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  48. package/.cursor/skills/daily/SKILL.md +12 -0
  49. package/.cursor/skills/debug/SKILL.md +13 -0
  50. package/.cursor/skills/discovery/SKILL.md +13 -0
  51. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  52. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  53. package/.cursor/skills/domain-model/SKILL.md +13 -0
  54. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  55. package/.cursor/skills/eli5/SKILL.md +13 -0
  56. package/.cursor/skills/eval/SKILL.md +12 -0
  57. package/.cursor/skills/evolve/SKILL.md +13 -0
  58. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  60. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  61. package/.cursor/skills/grill/SKILL.md +13 -0
  62. package/.cursor/skills/hook-development/SKILL.md +13 -0
  63. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  64. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  65. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  66. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  67. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  68. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  69. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  70. package/.cursor/skills/plan/SKILL.md +13 -0
  71. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  72. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  73. package/.cursor/skills/reconcile/SKILL.md +12 -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 +269 -0
  91. package/README.md +18 -15
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -1
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/memory-proposal-collector.md +6 -4
  98. package/agents/session-reviewer.md +7 -1
  99. package/agents/test-writer.md +2 -1
  100. package/agents/ui-developer.md +2 -1
  101. package/commands/bootstrap.md +2 -2
  102. package/commands/close.md +3 -1
  103. package/commands/eli5.md +33 -0
  104. package/commands/go.md +1 -1
  105. package/commands/journey-audit.md +43 -0
  106. package/commands/release.md +5 -3
  107. package/commands/test.md +2 -2
  108. package/docs/USER-GUIDE.md +2 -2
  109. package/docs/ci-setup.md +14 -0
  110. package/docs/codex-setup.md +64 -0
  111. package/docs/components.md +7 -6
  112. package/docs/cursor-setup.md +26 -47
  113. package/docs/events-schema.md +76 -4
  114. package/docs/github-mirror-protection.md +197 -0
  115. package/docs/pi-setup.md +2 -0
  116. package/docs/rule-authoring.md +3 -1
  117. package/docs/scope-collision-guard.md +52 -5
  118. package/docs/session-config-reference.md +57 -12
  119. package/docs/session-config-template.md +4 -3
  120. package/docs/telemetry.md +22 -0
  121. package/hooks/_lib/lock-bootstrap.mjs +26 -16
  122. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  123. package/hooks/enforce-scope.mjs +64 -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 +12 -2
  128. package/hooks/on-session-end.mjs +237 -104
  129. package/hooks/on-session-start.mjs +404 -110
  130. package/hooks/on-stop.mjs +48 -9
  131. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  132. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  133. package/hooks/pre-auq-clarity.mjs +787 -0
  134. package/hooks/pre-bash-issue-budget.mjs +133 -21
  135. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  136. package/hooks/pre-bash-staging-fence.mjs +4 -0
  137. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  138. package/hooks/skill-invocation-telemetry.mjs +21 -10
  139. package/monitors/monitors.json +6 -0
  140. package/package.json +3 -1
  141. package/pi/prompts/eli5.md +12 -0
  142. package/pi/prompts/journey-audit.md +12 -0
  143. package/rules/_index.md +9 -1
  144. package/rules/always-on/ask-via-tool.md +62 -0
  145. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  146. package/rules/always-on/build-value.md +47 -0
  147. package/rules/always-on/cross-session-messaging.md +59 -0
  148. package/rules/always-on/loop-and-monitor.md +221 -0
  149. package/rules/always-on/parallel-sessions.md +142 -12
  150. package/rules/always-on/receiving-review.md +108 -0
  151. package/rules/always-on/test-value.md +40 -0
  152. package/rules/always-on/verification-before-completion.md +77 -0
  153. package/scripts/archive-closed-prds.mjs +258 -18
  154. package/scripts/auq-audit.mjs +825 -0
  155. package/scripts/autopilot.mjs +12 -8
  156. package/scripts/backfill-evidence-digest.mjs +376 -0
  157. package/scripts/cursor-install.mjs +89 -48
  158. package/scripts/export-hw-learnings.mjs +143 -2
  159. package/scripts/express-path.mjs +299 -0
  160. package/scripts/generate-cursor-adapter.mjs +253 -0
  161. package/scripts/github-protection-audit.mjs +358 -0
  162. package/scripts/lib/auq/clarity.mjs +1314 -0
  163. package/scripts/lib/auq/parse.mjs +1006 -0
  164. package/scripts/lib/auq/schema.mjs +1457 -0
  165. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  166. package/scripts/lib/build-live-signals.mjs +24 -5
  167. package/scripts/lib/ci-status-banner.mjs +221 -68
  168. package/scripts/lib/command-blocker.mjs +70 -0
  169. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  170. package/scripts/lib/config/reconcile.mjs +79 -4
  171. package/scripts/lib/config/section-extractor.mjs +235 -36
  172. package/scripts/lib/config/vault-integration.mjs +12 -1
  173. package/scripts/lib/config-schema.mjs +9 -1
  174. package/scripts/lib/config.mjs +57 -6
  175. package/scripts/lib/convergence-monitor.mjs +13 -2
  176. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  177. package/scripts/lib/dispatcher/cli.mjs +2 -2
  178. package/scripts/lib/dispatcher/rank.mjs +4 -7
  179. package/scripts/lib/express-path.mjs +327 -0
  180. package/scripts/lib/file-lock.mjs +22 -4
  181. package/scripts/lib/gates/gate-full.mjs +84 -11
  182. package/scripts/lib/gates/gate-helpers.mjs +93 -21
  183. package/scripts/lib/git-config-drift.mjs +134 -5
  184. package/scripts/lib/host-identity.mjs +247 -2
  185. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  186. package/scripts/lib/io.mjs +239 -0
  187. package/scripts/lib/issue-budget.mjs +284 -31
  188. package/scripts/lib/learnings/io.mjs +55 -10
  189. package/scripts/lib/learnings/schema.mjs +95 -28
  190. package/scripts/lib/lock-reaper.mjs +7 -1
  191. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  192. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  193. package/scripts/lib/memory-banner.mjs +5 -2
  194. package/scripts/lib/memory-paths.mjs +15 -6
  195. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  196. package/scripts/lib/owner-interview.mjs +78 -32
  197. package/scripts/lib/peer-discovery.mjs +73 -22
  198. package/scripts/lib/platform.mjs +72 -9
  199. package/scripts/lib/plugin-root.mjs +143 -19
  200. package/scripts/lib/project-hygiene.mjs +104 -4
  201. package/scripts/lib/quality-gate.mjs +271 -13
  202. package/scripts/lib/reconcile/emitter.mjs +87 -19
  203. package/scripts/lib/reconcile/engine.mjs +281 -13
  204. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  205. package/scripts/lib/reconcile/renderer.mjs +165 -7
  206. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  207. package/scripts/lib/reconcile/writer.mjs +415 -84
  208. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  209. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  210. package/scripts/lib/resource-probe.mjs +18 -2
  211. package/scripts/lib/rule-loader.mjs +37 -2
  212. package/scripts/lib/rules-sync.mjs +51 -8
  213. package/scripts/lib/scope-gate.mjs +90 -0
  214. package/scripts/lib/session-close-backfill.mjs +369 -28
  215. package/scripts/lib/session-discovery.mjs +13 -3
  216. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  217. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  218. package/scripts/lib/session-id.mjs +30 -14
  219. package/scripts/lib/session-identity/own-session.mjs +159 -0
  220. package/scripts/lib/session-lock.mjs +85 -30
  221. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  222. package/scripts/lib/session-schema/validator.mjs +40 -0
  223. package/scripts/lib/session-start-probes.mjs +608 -0
  224. package/scripts/lib/session-transition.mjs +277 -0
  225. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  226. package/scripts/lib/spiral-carryover.mjs +111 -9
  227. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  228. package/scripts/lib/state-md/mission-status.mjs +467 -72
  229. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  230. package/scripts/lib/state-md.mjs +12 -2
  231. package/scripts/lib/telemetry/sync.mjs +46 -8
  232. package/scripts/lib/validate/check-agents.mjs +66 -0
  233. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  234. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  235. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  236. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  237. package/scripts/lib/validate/check-hooks-symmetry.mjs +59 -63
  238. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  239. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  240. package/scripts/lib/validate/check-rules.mjs +153 -9
  241. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  242. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  243. package/scripts/lib/validate/repo-files.mjs +275 -0
  244. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  245. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  246. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  247. package/scripts/lib/vault-mirror/process.mjs +99 -43
  248. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  249. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  250. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  251. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  252. package/scripts/lib/vcs-repo-spec.mjs +182 -13
  253. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  254. package/scripts/lib/wave-resource-gate.mjs +67 -73
  255. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  256. package/scripts/materialize-wave-scope.mjs +478 -0
  257. package/scripts/mcp-server.sh +11 -2
  258. package/scripts/parse-config.mjs +65 -0
  259. package/scripts/release.mjs +443 -122
  260. package/scripts/run-quality-gate.mjs +14 -0
  261. package/scripts/token-audit.sh +9 -2
  262. package/scripts/validate-plugin.mjs +6 -0
  263. package/scripts/validate-wave-scope.mjs +73 -1
  264. package/scripts/vault-backfill.mjs +32 -5
  265. package/scripts/vault-mirror.mjs +203 -34
  266. package/skills/_shared/monitor-patterns.md +31 -5
  267. package/skills/_shared/parallel-aware-auq.md +31 -25
  268. package/skills/_shared/parallel-aware-preamble.md +35 -4
  269. package/skills/_shared/platform-tools.md +11 -5
  270. package/skills/_shared/state-ownership.md +61 -8
  271. package/skills/autopilot/SKILL.md +5 -1
  272. package/skills/bootstrap/SKILL.md +5 -4
  273. package/skills/bootstrap/_shared-template.md +18 -10
  274. package/skills/bootstrap/deep-template.md +10 -6
  275. package/skills/bootstrap/fast-template.md +15 -8
  276. package/skills/bootstrap/standard-template.md +10 -6
  277. package/skills/brainstorm/SKILL.md +18 -18
  278. package/skills/brainstorm/soul.md +12 -0
  279. package/skills/claude-md-drift-check/checker.mjs +39 -11
  280. package/skills/discovery/SKILL.md +28 -24
  281. package/skills/dispatcher/SKILL.md +1 -1
  282. package/skills/eli5/SKILL.md +43 -0
  283. package/skills/evolve/SKILL.md +8 -9
  284. package/skills/gitlab-ops/SKILL.md +30 -26
  285. package/skills/grill/SKILL.md +6 -6
  286. package/skills/grill/soul.md +16 -0
  287. package/skills/journey-audit/SKILL.md +269 -0
  288. package/skills/memory-cleanup/SKILL.md +2 -2
  289. package/skills/npm-publish/SKILL.md +4 -4
  290. package/skills/peekaboo-driver/SKILL.md +18 -6
  291. package/skills/persona-panel/SKILL.md +1 -1
  292. package/skills/plan/SKILL.md +18 -16
  293. package/skills/plan/mode-feature.md +1 -1
  294. package/skills/plan/mode-new.md +35 -23
  295. package/skills/plan/soul.md +12 -0
  296. package/skills/reconcile/SKILL.md +44 -4
  297. package/skills/session-end/SKILL.md +70 -24
  298. package/skills/session-end/metrics-collection.md +7 -4
  299. package/skills/session-end/phase-3-6-tail.md +48 -5
  300. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  301. package/skills/session-plan/SKILL.md +6 -1
  302. package/skills/session-plan/wave-template.md +1 -0
  303. package/skills/session-start/SKILL.md +96 -48
  304. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  305. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  306. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  307. package/skills/session-start/phase-8-5-express-path.md +77 -12
  308. package/skills/session-start/soul.md +110 -0
  309. package/skills/test-runner/SKILL.md +2 -2
  310. package/skills/using-orchestrator/SKILL.md +1 -1
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +4 -2
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +129 -24
  315. package/skills/write-executable-plan/SKILL.md +6 -6
  316. package/templates/_shared/journey-manifest.md +110 -0
  317. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  318. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  319. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  320. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  321. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  322. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  323. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  324. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  325. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  326. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  327. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  328. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  329. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  330. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  331. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  332. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  333. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  334. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  335. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  336. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  337. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  338. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  339. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  340. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  341. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  342. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  343. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  344. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
  345. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -36,6 +36,11 @@ import { acquire, release, buildLockOwnerProof } from '../session-lock.mjs';
36
36
  import { emitEvent } from '../events.mjs';
37
37
  import { main as gcMain } from '../../gc-stale-worktrees.mjs';
38
38
  import { SEMANTIC_ID_RE } from '../session-id.mjs';
39
+ import { repoPathHash } from '../session-registry.mjs';
40
+ // Marker location is owned by its READER (session-end Phase 4a) — importing the
41
+ // constant from there keeps writer and reader on one string. The dependency runs
42
+ // heavy→lean (this pipeline → the dependency-free cleanup helper), never back.
43
+ import { PROMOTION_MARKER_RELPATH } from '../session-end/worktree-cleanup.mjs';
39
44
 
40
45
  // ---------------------------------------------------------------------------
41
46
  // Type definitions
@@ -547,6 +552,134 @@ export async function runStoryPipeline(context, opts = {}) {
547
552
  /** Valid git branch character set — mirrors isValidBranch from session-id.mjs. */
548
553
  const ENTER_WORKTREE_BRANCH_RE = /^[a-zA-Z0-9._/-]+$/;
549
554
 
555
+ /**
556
+ * Raised when the promotion branch `so/<sessionId>` is already checked out by
557
+ * another worktree. Refusing is the point: adopting a foreign target worktree
558
+ * would put two sessions on one branch (PSA-002 territory), so the caller gets
559
+ * the conflicting path by name instead of a silent takeover.
560
+ */
561
+ export class WorktreePromotionBranchError extends Error {
562
+ /**
563
+ * @param {string} message
564
+ * @param {{branch: string, checkedOutAt: string}} detail
565
+ */
566
+ constructor(message, { branch, checkedOutAt } = {}) {
567
+ super(message);
568
+ this.name = 'WorktreePromotionBranchError';
569
+ this.branch = branch;
570
+ this.checkedOutAt = checkedOutAt;
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Map of branch name → worktree path for every branch currently checked out by
576
+ * ANY worktree of `repoRoot` (#1067).
577
+ *
578
+ * Deliberately NOT `listWorktrees()` from `../worktree/listing.mjs`: that helper
579
+ * runs `git worktree list --porcelain` in `process.cwd()` (it has no repo
580
+ * parameter), and `enterWorktree` takes `repoRoot` explicitly precisely to avoid
581
+ * CWD drift (#219) — so reusing it would query whichever repo the process
582
+ * happens to sit in. Its `$` seam also has a different shape (`$({cwd})` returns
583
+ * the tag) than `enterWorktree`'s (`exec` IS the tag), so the two cannot share
584
+ * one injected executor. Only the `branch` lines are parsed here; `HEAD`,
585
+ * `detached`, `bare` and `locked` records carry nothing this decision needs.
586
+ *
587
+ * @param {Function} exec zx-like template-tag executor (the DI seam).
588
+ * @param {string} repoRoot Absolute path to the source git repository.
589
+ * @returns {Promise<Map<string, string>>} Branch names (without the
590
+ * `refs/heads/` prefix) → the worktree path that has them checked out.
591
+ * Empty on git failure — the caller then behaves exactly as it did before this
592
+ * check existed, so a broken listing degrades to git's own loud
593
+ * `already used by worktree` error rather than to a silent wrong branch.
594
+ */
595
+ async function listCheckedOutBranches(exec, repoRoot) {
596
+ let stdout;
597
+ try {
598
+ const result = await exec`git -C ${repoRoot} worktree list --porcelain`;
599
+ stdout = String(result?.stdout ?? '');
600
+ } catch {
601
+ return new Map();
602
+ }
603
+
604
+ const branches = new Map();
605
+ let currentPath = null;
606
+ for (const rawLine of stdout.split('\n')) {
607
+ const line = rawLine.trimEnd();
608
+ const wtMatch = /^worktree (.+)$/.exec(line);
609
+ if (wtMatch) {
610
+ currentPath = wtMatch[1];
611
+ continue;
612
+ }
613
+ const branchMatch = /^branch refs\/heads\/(.+)$/.exec(line);
614
+ if (branchMatch) branches.set(branchMatch[1], currentPath ?? '');
615
+ }
616
+ return branches;
617
+ }
618
+
619
+ /**
620
+ * Does `<ref>` resolve in `repoRoot`? Wraps `git rev-parse --verify --quiet`.
621
+ *
622
+ * @param {Function} exec
623
+ * @param {string} repoRoot
624
+ * @param {string} ref
625
+ * @returns {Promise<boolean>} false on any git failure (the ref is absent, or
626
+ * git could not be asked — both lead to the create-new path, whose failure
627
+ * mode is git's own loud error rather than a silent wrong checkout).
628
+ */
629
+ async function refExists(exec, repoRoot, ref) {
630
+ try {
631
+ await exec`git -C ${repoRoot} rev-parse --verify --quiet ${ref}`;
632
+ return true;
633
+ } catch {
634
+ return false;
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Record, inside the freshly created worktree, that it was auto-promoted —
640
+ * and from where. Best-effort: a failure to write the marker degrades Phase 4a
641
+ * detection to the legacy basename key, it never fails the promotion itself.
642
+ *
643
+ * No absolute source path is stored: `source_root_hash` is `repoPathHash()`, the
644
+ * same stable SHA-256 the session registry uses to correlate repos without
645
+ * exposing the operator's filesystem layout.
646
+ *
647
+ * @param {object} params
648
+ * @param {string} params.wtPath Freshly created worktree.
649
+ * @param {string} params.sourceRoot Resolved source checkout.
650
+ * @param {string} params.sessionId Session label the worktree was created for.
651
+ * @param {string} params.branch Branch the worktree actually landed on.
652
+ * @returns {boolean} true when the marker was written.
653
+ */
654
+ function writePromotionMarker({ wtPath, sourceRoot, sessionId, branch }) {
655
+ try {
656
+ const markerPath = path.join(wtPath, PROMOTION_MARKER_RELPATH);
657
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
658
+ fs.writeFileSync(
659
+ markerPath,
660
+ JSON.stringify(
661
+ {
662
+ source_root_hash: repoPathHash(sourceRoot),
663
+ source_root_basename: path.basename(sourceRoot),
664
+ source_session_id: sessionId,
665
+ branch,
666
+ promoted_at: new Date().toISOString(),
667
+ },
668
+ null,
669
+ 2,
670
+ ) + '\n',
671
+ 'utf8',
672
+ );
673
+ return true;
674
+ } catch (err) {
675
+ console.warn(
676
+ `enterWorktree: could not write promotion marker in ${wtPath} (${err.message}) — ` +
677
+ 'session-end Phase 4a falls back to basename detection',
678
+ );
679
+ return false;
680
+ }
681
+ }
682
+
550
683
  /**
551
684
  * Create a sibling git worktree for Worktree-Auto-Promotion (#574, Epic #568 P3.1).
552
685
  *
@@ -573,10 +706,25 @@ const ENTER_WORKTREE_BRANCH_RE = /^[a-zA-Z0-9._/-]+$/;
573
706
  * check as `setupWorktree` (CWE-23 / SEC-013 defence-in-depth). Throws
574
707
  * `WorktreeBoundaryError` if the computed path escapes `basePath`.
575
708
  *
576
- * Branch handling: if the branch already exists (verified via
577
- * `git rev-parse --verify <branch>`), use `git worktree add <wtPath> <branch>`
578
- * (reuse). Otherwise use `git worktree add -b <branch> <wtPath>` (create new).
579
- * This differs from `setupWorktree`, which always passes `-b`.
709
+ * Branch handling — three cases, decided in this order (#1067):
710
+ * 1. `<branch>` is already checked out by ANY worktree of `repoRoot` (the
711
+ * normal session-start Phase 0.5 case, where `branch` is the CURRENT HEAD):
712
+ * git refuses a second checkout of the same branch
713
+ * (`fatal: '<branch>' is already used by worktree at '<repoRoot>'`). Treat
714
+ * `<branch>` as a START POINT only and create a fresh promotion branch
715
+ * `so/<sessionId>` at it: `git worktree add -b so/<sessionId> <wtPath> <branch>`.
716
+ * The source branch and the source worktree are left untouched.
717
+ * If `so/<sessionId>` ALREADY exists (a worktree directory was removed but
718
+ * its branch survived), it is REUSED — `git worktree add <wtPath>
719
+ * so/<sessionId>`, `reusedBranch: true` in the return — because `-b` would
720
+ * abort with `fatal: a branch named 'so/<sessionId>' already exists`.
721
+ * If it exists AND is checked out by some worktree, this throws
722
+ * `WorktreePromotionBranchError` naming that path: adopting a foreign
723
+ * target worktree is refused, never silently taken over.
724
+ * 2. `<branch>` exists but is not checked out (verified via
725
+ * `git rev-parse --verify <branch>`): `git worktree add <wtPath> <branch>`.
726
+ * 3. `<branch>` does not exist: `git worktree add -b <branch> <wtPath>`.
727
+ * Cases 2/3 differ from `setupWorktree`, which always passes `-b`.
580
728
  *
581
729
  * @param {object} params
582
730
  * @param {string} params.basePath - Parent directory where the new worktree goes (absolute).
@@ -585,9 +733,22 @@ const ENTER_WORKTREE_BRANCH_RE = /^[a-zA-Z0-9._/-]+$/;
585
733
  * @param {string} params.repoRoot - Path to the source git repository (passed explicitly to avoid CWD drift per #219).
586
734
  * @param {object} [opts]
587
735
  * @param {Function} [opts.$] - zx-like template-tag executor (DI seam); falls back to lazy `await import('zx')`.
588
- * @returns {Promise<{ wtPath: string, reused: boolean }>}
736
+ * Every freshly created worktree also gets a `.orchestrator/promoted-from.json`
737
+ * marker (see {@link PROMOTION_MARKER_RELPATH}) so session-end Phase 4a can
738
+ * still recognise it after the #1069 process boundary hands the worktree to a
739
+ * session with a different id. Marker writing is best-effort and never fails
740
+ * the promotion.
741
+ *
742
+ * @returns {Promise<{ wtPath: string, reused: boolean, branch?: string, promotedFrom?: string, reusedBranch?: true }>}
743
+ * `branch` is the branch the new worktree actually landed on — equal to the
744
+ * `branch` param except in case 1 above, where it is `so/<sessionId>` and
745
+ * `promotedFrom` carries the requested source branch. `reusedBranch` is
746
+ * present (and `true`) only when an existing `so/<sessionId>` was checked out
747
+ * rather than created. All three are absent on the `reused: true` path (no
748
+ * branch was chosen — the worktree pre-existed).
589
749
  * @throws {TypeError} when any required param is missing or fails validation.
590
750
  * @throws {WorktreeBoundaryError} when the computed worktree path escapes `basePath`.
751
+ * @throws {WorktreePromotionBranchError} when `so/<sessionId>` is checked out elsewhere.
591
752
  */
592
753
  export async function enterWorktree({ basePath, sessionId, branch, repoRoot } = {}, opts = {}) {
593
754
  // -------------------------------------------------------------------------
@@ -686,28 +847,91 @@ export async function enterWorktree({ basePath, sessionId, branch, repoRoot } =
686
847
  }
687
848
 
688
849
  // -------------------------------------------------------------------------
689
- // Step 5: Detect whether branch already exists, then `git worktree add`.
850
+ // Step 5: Is `branch` already checked out elsewhere? (#1067)
851
+ //
852
+ // Phase 0.5 passes the CURRENT HEAD as `branch`, so in the normal promotion
853
+ // case it is checked out by `repoRoot` itself and git would refuse a second
854
+ // checkout. Then `branch` is only a start point and the worktree lands on a
855
+ // fresh `so/<sessionId>`; the source branch/worktree stay untouched.
690
856
  // -------------------------------------------------------------------------
691
- let branchExists = false;
692
- try {
693
- await exec`git -C ${repoRoot} rev-parse --verify ${branch}`;
694
- branchExists = true;
695
- } catch {
696
- // Branch does not exist — fall through to create-new path with `-b`.
857
+ const checkedOutBranches = await listCheckedOutBranches(exec, repoRoot);
858
+ const promoted = checkedOutBranches.has(branch);
859
+ const targetBranch = promoted ? `so/${sessionId}` : branch;
860
+
861
+ // -------------------------------------------------------------------------
862
+ // Step 6: Decide the `git worktree add` argv.
863
+ //
864
+ // On the promotion path the target is `so/<sessionId>`, which can be in any
865
+ // of THREE states — and only the first was handled before:
866
+ // (a) absent → create it at `<branch>`: `add -b so/<id> <wt> <branch>`
867
+ // (b) exists, free → REUSE it: `add <wt> so/<id>`. This is the survivor
868
+ // of a worktree whose directory was removed while its branch stayed
869
+ // behind (`git worktree remove` does not delete the branch); `-b` would
870
+ // abort Phase 0.5 with `fatal: a branch named 'so/<id>' already exists`.
871
+ // (c) exists, checked out elsewhere → REFUSE. Adopting a foreign target
872
+ // worktree is exactly what Phase 0.5 must not do.
873
+ // -------------------------------------------------------------------------
874
+ let reusedBranch = false;
875
+ if (promoted) {
876
+ const checkedOutAt = checkedOutBranches.get(targetBranch);
877
+ if (checkedOutAt !== undefined) {
878
+ throw new WorktreePromotionBranchError(
879
+ `enterWorktree: promotion branch '${targetBranch}' is already checked out at '${checkedOutAt}' — refusing to adopt a foreign worktree`,
880
+ { branch: targetBranch, checkedOutAt },
881
+ );
882
+ }
883
+ reusedBranch = await refExists(exec, repoRoot, `refs/heads/${targetBranch}`);
697
884
  }
698
885
 
699
- if (branchExists) {
886
+ // A branch that a worktree has checked out exists by construction, so the
887
+ // source-branch rev-parse probe is skipped on the promotion path.
888
+ let branchExists = promoted;
889
+ if (!promoted) {
890
+ try {
891
+ await exec`git -C ${repoRoot} rev-parse --verify ${branch}`;
892
+ branchExists = true;
893
+ } catch {
894
+ // Branch does not exist — fall through to create-new path with `-b`.
895
+ }
896
+ }
897
+
898
+ if (promoted && reusedBranch) {
899
+ await exec`git -C ${repoRoot} worktree add ${wtPath} ${targetBranch}`;
900
+ } else if (promoted) {
901
+ await exec`git -C ${repoRoot} worktree add -b ${targetBranch} ${wtPath} ${branch}`;
902
+ } else if (branchExists) {
700
903
  await exec`git -C ${repoRoot} worktree add ${wtPath} ${branch}`;
701
904
  } else {
702
905
  await exec`git -C ${repoRoot} worktree add -b ${branch} ${wtPath}`;
703
906
  }
704
907
 
705
908
  // -------------------------------------------------------------------------
706
- // Step 6: WARN to stderr (PRD §3 P3 Gherkin row-1 + #574 DoD).
909
+ // Step 6b: Record the promotion FACT inside the new worktree (#1069 boundary).
910
+ // Session-end Phase 4a runs in a session whose id is NOT this one, so nothing
911
+ // in the path or the branch identifies the worktree to it — the marker does.
912
+ // -------------------------------------------------------------------------
913
+ writePromotionMarker({
914
+ wtPath,
915
+ sourceRoot: resolvedRepoRoot,
916
+ sessionId,
917
+ branch: targetBranch,
918
+ });
919
+
920
+ // -------------------------------------------------------------------------
921
+ // Step 7: WARN to stderr (PRD §3 P3 Gherkin row-1 + #574 DoD).
707
922
  // -------------------------------------------------------------------------
708
923
  console.warn(
709
- `enterWorktree: created sibling worktree at ${wtPath} (branch=${branch}, sessionId=${sessionId})`,
924
+ promoted
925
+ ? `enterWorktree: created sibling worktree at ${wtPath} (branch=${targetBranch}, promoted from ${branch}, sessionId=${sessionId})`
926
+ : `enterWorktree: created sibling worktree at ${wtPath} (branch=${branch}, sessionId=${sessionId})`,
710
927
  );
711
928
 
712
- return { wtPath, reused: false };
929
+ if (promoted) {
930
+ const result = { wtPath, reused: false, branch: targetBranch, promotedFrom: branch };
931
+ // Present only when it happened, so the common shape stays byte-identical
932
+ // for every existing consumer and strict-equality pin.
933
+ if (reusedBranch) result.reusedBranch = true;
934
+ return result;
935
+ }
936
+ return { wtPath, reused: false, branch };
713
937
  }
@@ -10,7 +10,8 @@
10
10
  * - Synchronous file I/O for STATE.md / sessions.jsonl / bootstrap.lock.
11
11
  * The async wrapper exists only because `scanBacklog` is async.
12
12
  * - Logging is NOT this helper's job — silent graceful-null on every error.
13
- * - Paths are resolved absolutely from process.cwd() if relative.
13
+ * - Relative paths resolve against `opts.repoRoot` (which itself defaults to
14
+ * process.cwd()); an explicit ABSOLUTE path always wins over the root.
14
15
  */
15
16
 
16
17
  import { existsSync, readFileSync } from 'node:fs';
@@ -32,9 +33,16 @@ import { scanBacklog, DEFAULT_BACKLOG_LIMIT } from './backlog-scan.mjs';
32
33
  * null/[] value to the Signals object.
33
34
  *
34
35
  * @param {object} [opts]
35
- * @param {string} [opts.statePath] — defaults to '.claude/STATE.md'
36
- * @param {string} [opts.sessionsPath] — defaults to '.orchestrator/metrics/sessions.jsonl'
37
- * @param {string} [opts.lockPath] — defaults to '.orchestrator/bootstrap.lock'
36
+ * @param {string} [opts.repoRoot] — absolute project root every relative
37
+ * path below resolves against, and the root forwarded to `scanBacklog`.
38
+ * Defaults to `process.cwd()`. Without it (#1071) a caller running from a
39
+ * worktree or a subdirectory silently read a DIFFERENT repo's STATE.md and
40
+ * sessions.jsonl than the one it was reporting on — measured as
41
+ * `recentSessions: []` against a checkout holding 245 session records.
42
+ * An explicit absolute `statePath`/`sessionsPath`/`lockPath` still wins.
43
+ * @param {string} [opts.statePath] — defaults to '<repoRoot>/.claude/STATE.md'
44
+ * @param {string} [opts.sessionsPath] — defaults to '<repoRoot>/.orchestrator/metrics/sessions.jsonl'
45
+ * @param {string} [opts.lockPath] — defaults to '<repoRoot>/.orchestrator/bootstrap.lock'
38
46
  * @param {Array} [opts.learnings] — pre-surfaced top-N learnings; defaults to []
39
47
  * @param {number} [opts.backlogLimit] — passed to scanBacklog; defaults to
40
48
  * `DEFAULT_BACKLOG_LIMIT` from backlog-scan.mjs (never a local copy of that
@@ -46,15 +54,24 @@ import { scanBacklog, DEFAULT_BACKLOG_LIMIT } from './backlog-scan.mjs';
46
54
  * @returns {Promise<import('./mode-selector.mjs').Signals>}
47
55
  */
48
56
  export async function buildLiveSignals(opts = {}) {
57
+ const repoRoot =
58
+ typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0
59
+ ? opts.repoRoot
60
+ : process.cwd();
61
+ // `resolve(root, p)` returns `p` unchanged when `p` is absolute — explicit
62
+ // per-file overrides therefore keep precedence over repoRoot.
49
63
  const statePath = resolve(
64
+ repoRoot,
50
65
  typeof opts.statePath === 'string' ? opts.statePath : '.claude/STATE.md'
51
66
  );
52
67
  const sessionsPath = resolve(
68
+ repoRoot,
53
69
  typeof opts.sessionsPath === 'string'
54
70
  ? opts.sessionsPath
55
71
  : '.orchestrator/metrics/sessions.jsonl'
56
72
  );
57
73
  const lockPath = resolve(
74
+ repoRoot,
58
75
  typeof opts.lockPath === 'string' ? opts.lockPath : '.orchestrator/bootstrap.lock'
59
76
  );
60
77
  const learnings = Array.isArray(opts.learnings) ? opts.learnings : [];
@@ -137,7 +154,9 @@ export async function buildLiveSignals(opts = {}) {
137
154
  let backlog = null;
138
155
 
139
156
  try {
140
- backlog = await _scan({ limit: backlogLimit });
157
+ // `repoRoot` is forwarded so VCS detection and the `-R` host-pinning spec
158
+ // inside scanBacklog answer about the SAME repo the signals describe.
159
+ backlog = await _scan({ limit: backlogLimit, repoRoot });
141
160
  } catch {
142
161
  // Branch 6: scanBacklog threw — backlog stays null
143
162
  }