oh-my-copilot 4.13.2 → 4.13.21

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 (311) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/bridge/cli.cjs +2522 -949
  5. package/bridge/mcp-server.cjs +97 -9
  6. package/bridge/runtime-cli.cjs +1256 -133
  7. package/bridge/team.js +1241 -65
  8. package/dist/__tests__/auto-slash-aliases.test.js +91 -0
  9. package/dist/__tests__/auto-slash-aliases.test.js.map +1 -1
  10. package/dist/__tests__/hud/labels.test.d.ts +2 -0
  11. package/dist/__tests__/hud/labels.test.d.ts.map +1 -0
  12. package/dist/__tests__/hud/labels.test.js +158 -0
  13. package/dist/__tests__/hud/labels.test.js.map +1 -0
  14. package/dist/__tests__/hud/render-rate-limits-priority.test.js +26 -0
  15. package/dist/__tests__/hud/render-rate-limits-priority.test.js.map +1 -1
  16. package/dist/__tests__/hud-marketplace-resolution.test.js +34 -0
  17. package/dist/__tests__/hud-marketplace-resolution.test.js.map +1 -1
  18. package/dist/__tests__/installer-hud-skip.test.js +6 -0
  19. package/dist/__tests__/installer-hud-skip.test.js.map +1 -1
  20. package/dist/__tests__/installer-omc-reference.test.js +1 -1
  21. package/dist/__tests__/installer-omc-reference.test.js.map +1 -1
  22. package/dist/__tests__/mnemosyne/detector.test.js +2 -0
  23. package/dist/__tests__/mnemosyne/detector.test.js.map +1 -1
  24. package/dist/__tests__/notepad.test.js +2 -2
  25. package/dist/__tests__/notepad.test.js.map +1 -1
  26. package/dist/__tests__/pre-tool-enforcer.test.js +84 -1
  27. package/dist/__tests__/pre-tool-enforcer.test.js.map +1 -1
  28. package/dist/__tests__/ralph-prd-mandatory.test.js +11 -1
  29. package/dist/__tests__/ralph-prd-mandatory.test.js.map +1 -1
  30. package/dist/__tests__/ralph-prd.test.js +40 -2
  31. package/dist/__tests__/ralph-prd.test.js.map +1 -1
  32. package/dist/__tests__/skills.test.js +363 -4
  33. package/dist/__tests__/skills.test.js.map +1 -1
  34. package/dist/__tests__/state-root-resolution.test.js +56 -6
  35. package/dist/__tests__/state-root-resolution.test.js.map +1 -1
  36. package/dist/autoresearch/__tests__/runtime.test.js +225 -28
  37. package/dist/autoresearch/__tests__/runtime.test.js.map +1 -1
  38. package/dist/autoresearch/runtime.d.ts.map +1 -1
  39. package/dist/autoresearch/runtime.js +9 -5
  40. package/dist/autoresearch/runtime.js.map +1 -1
  41. package/dist/cli/autoresearch.d.ts +1 -1
  42. package/dist/cli/autoresearch.d.ts.map +1 -1
  43. package/dist/cli/autoresearch.js +4 -4
  44. package/dist/cli/commands/team.d.ts +1 -0
  45. package/dist/cli/commands/team.d.ts.map +1 -1
  46. package/dist/cli/commands/team.js +14 -2
  47. package/dist/cli/commands/team.js.map +1 -1
  48. package/dist/cli/index.js +3 -0
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/cli/team.d.ts +6 -0
  51. package/dist/cli/team.d.ts.map +1 -1
  52. package/dist/cli/team.js +23 -1
  53. package/dist/cli/team.js.map +1 -1
  54. package/dist/config/__tests__/loader.test.js.map +1 -1
  55. package/dist/features/boulder-state/constants.d.ts +1 -1
  56. package/dist/features/boulder-state/constants.d.ts.map +1 -1
  57. package/dist/features/builtin-skills/skills.d.ts.map +1 -1
  58. package/dist/features/builtin-skills/skills.js +11 -3
  59. package/dist/features/builtin-skills/skills.js.map +1 -1
  60. package/dist/features/builtin-skills/types.d.ts +7 -0
  61. package/dist/features/builtin-skills/types.d.ts.map +1 -1
  62. package/dist/features/magic-keywords.d.ts.map +1 -1
  63. package/dist/features/magic-keywords.js +30 -1
  64. package/dist/features/magic-keywords.js.map +1 -1
  65. package/dist/features/rate-limit-wait/tmux-detector.d.ts +4 -4
  66. package/dist/features/rate-limit-wait/tmux-detector.js +9 -9
  67. package/dist/features/rate-limit-wait/tmux-detector.js.map +1 -1
  68. package/dist/hooks/__tests__/bridge-routing.test.js +156 -0
  69. package/dist/hooks/__tests__/bridge-routing.test.js.map +1 -1
  70. package/dist/hooks/auto-slash-command/constants.d.ts.map +1 -1
  71. package/dist/hooks/auto-slash-command/constants.js +1 -0
  72. package/dist/hooks/auto-slash-command/constants.js.map +1 -1
  73. package/dist/hooks/auto-slash-command/executor.d.ts.map +1 -1
  74. package/dist/hooks/auto-slash-command/executor.js +29 -0
  75. package/dist/hooks/auto-slash-command/executor.js.map +1 -1
  76. package/dist/hooks/bridge.d.ts.map +1 -1
  77. package/dist/hooks/bridge.js +184 -1
  78. package/dist/hooks/bridge.js.map +1 -1
  79. package/dist/hooks/learner/detector.js +1 -1
  80. package/dist/hooks/mode-registry/__tests__/session-isolation.test.js +16 -0
  81. package/dist/hooks/mode-registry/__tests__/session-isolation.test.js.map +1 -1
  82. package/dist/hooks/notepad/index.d.ts +4 -1
  83. package/dist/hooks/notepad/index.d.ts.map +1 -1
  84. package/dist/hooks/notepad/index.js +7 -4
  85. package/dist/hooks/notepad/index.js.map +1 -1
  86. package/dist/hooks/persistent-mode/__tests__/ralph-verification-flow.test.js +70 -3
  87. package/dist/hooks/persistent-mode/__tests__/ralph-verification-flow.test.js.map +1 -1
  88. package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
  89. package/dist/hooks/persistent-mode/index.js +10 -9
  90. package/dist/hooks/persistent-mode/index.js.map +1 -1
  91. package/dist/hooks/persistent-mode/stop-hook-blocking.test.js +64 -9
  92. package/dist/hooks/persistent-mode/stop-hook-blocking.test.js.map +1 -1
  93. package/dist/hooks/project-memory/__tests__/integration.test.js +117 -3
  94. package/dist/hooks/project-memory/__tests__/integration.test.js.map +1 -1
  95. package/dist/hooks/project-memory/__tests__/storage.test.js +11 -11
  96. package/dist/hooks/project-memory/__tests__/storage.test.js.map +1 -1
  97. package/dist/hooks/project-memory/index.d.ts.map +1 -1
  98. package/dist/hooks/project-memory/index.js +24 -12
  99. package/dist/hooks/project-memory/index.js.map +1 -1
  100. package/dist/hooks/ralph/index.d.ts +1 -1
  101. package/dist/hooks/ralph/index.d.ts.map +1 -1
  102. package/dist/hooks/ralph/index.js +1 -1
  103. package/dist/hooks/ralph/index.js.map +1 -1
  104. package/dist/hooks/ralph/loop.d.ts +6 -6
  105. package/dist/hooks/ralph/loop.d.ts.map +1 -1
  106. package/dist/hooks/ralph/loop.js +18 -18
  107. package/dist/hooks/ralph/loop.js.map +1 -1
  108. package/dist/hooks/ralph/prd.d.ts +23 -12
  109. package/dist/hooks/ralph/prd.d.ts.map +1 -1
  110. package/dist/hooks/ralph/prd.js +88 -42
  111. package/dist/hooks/ralph/prd.js.map +1 -1
  112. package/dist/hooks/session-end/__tests__/mode-state-cleanup.test.js +20 -0
  113. package/dist/hooks/session-end/__tests__/mode-state-cleanup.test.js.map +1 -1
  114. package/dist/hooks/session-end/index.d.ts +9 -0
  115. package/dist/hooks/session-end/index.d.ts.map +1 -1
  116. package/dist/hooks/session-end/index.js +67 -0
  117. package/dist/hooks/session-end/index.js.map +1 -1
  118. package/dist/hooks/skill-bridge.cjs +6 -5
  119. package/dist/hooks/skill-state/index.d.ts.map +1 -1
  120. package/dist/hooks/skill-state/index.js +1 -0
  121. package/dist/hooks/skill-state/index.js.map +1 -1
  122. package/dist/hud/__tests__/git-status.test.js +18 -0
  123. package/dist/hud/__tests__/git-status.test.js.map +1 -1
  124. package/dist/hud/elements/background.d.ts +3 -3
  125. package/dist/hud/elements/background.d.ts.map +1 -1
  126. package/dist/hud/elements/background.js +5 -4
  127. package/dist/hud/elements/background.js.map +1 -1
  128. package/dist/hud/elements/call-counts.d.ts +2 -2
  129. package/dist/hud/elements/call-counts.d.ts.map +1 -1
  130. package/dist/hud/elements/call-counts.js +7 -6
  131. package/dist/hud/elements/call-counts.js.map +1 -1
  132. package/dist/hud/elements/context.d.ts +3 -3
  133. package/dist/hud/elements/context.d.ts.map +1 -1
  134. package/dist/hud/elements/context.js +5 -4
  135. package/dist/hud/elements/context.js.map +1 -1
  136. package/dist/hud/elements/git.d.ts +2 -1
  137. package/dist/hud/elements/git.d.ts.map +1 -1
  138. package/dist/hud/elements/git.js +7 -6
  139. package/dist/hud/elements/git.js.map +1 -1
  140. package/dist/hud/elements/ralph.d.ts +2 -2
  141. package/dist/hud/elements/ralph.d.ts.map +1 -1
  142. package/dist/hud/elements/ralph.js +3 -2
  143. package/dist/hud/elements/ralph.js.map +1 -1
  144. package/dist/hud/elements/thinking.d.ts +2 -2
  145. package/dist/hud/elements/thinking.d.ts.map +1 -1
  146. package/dist/hud/elements/thinking.js +3 -2
  147. package/dist/hud/elements/thinking.js.map +1 -1
  148. package/dist/hud/elements/token-usage.d.ts +2 -2
  149. package/dist/hud/elements/token-usage.d.ts.map +1 -1
  150. package/dist/hud/elements/token-usage.js +3 -2
  151. package/dist/hud/elements/token-usage.js.map +1 -1
  152. package/dist/hud/render.d.ts.map +1 -1
  153. package/dist/hud/render.js +11 -10
  154. package/dist/hud/render.js.map +1 -1
  155. package/dist/hud/state.d.ts.map +1 -1
  156. package/dist/hud/state.js +18 -1
  157. package/dist/hud/state.js.map +1 -1
  158. package/dist/hud/types.d.ts +26 -0
  159. package/dist/hud/types.d.ts.map +1 -1
  160. package/dist/hud/types.js +58 -0
  161. package/dist/hud/types.js.map +1 -1
  162. package/dist/installer/__tests__/hud-cache-wrapper.test.d.ts +2 -0
  163. package/dist/installer/__tests__/hud-cache-wrapper.test.d.ts.map +1 -0
  164. package/dist/installer/__tests__/hud-cache-wrapper.test.js +235 -0
  165. package/dist/installer/__tests__/hud-cache-wrapper.test.js.map +1 -0
  166. package/dist/installer/__tests__/standalone-hook-reconcile.test.js +2 -0
  167. package/dist/installer/__tests__/standalone-hook-reconcile.test.js.map +1 -1
  168. package/dist/installer/index.d.ts.map +1 -1
  169. package/dist/installer/index.js +19 -7
  170. package/dist/installer/index.js.map +1 -1
  171. package/dist/lib/__tests__/worktree-paths.test.js +162 -39
  172. package/dist/lib/__tests__/worktree-paths.test.js.map +1 -1
  173. package/dist/lib/worktree-paths.d.ts +87 -9
  174. package/dist/lib/worktree-paths.d.ts.map +1 -1
  175. package/dist/lib/worktree-paths.js +209 -27
  176. package/dist/lib/worktree-paths.js.map +1 -1
  177. package/dist/team/__tests__/auto-merge-restart.test.d.ts +2 -0
  178. package/dist/team/__tests__/auto-merge-restart.test.d.ts.map +1 -0
  179. package/dist/team/__tests__/auto-merge-restart.test.js +128 -0
  180. package/dist/team/__tests__/auto-merge-restart.test.js.map +1 -0
  181. package/dist/team/__tests__/auto-merge.perf.test.d.ts +2 -0
  182. package/dist/team/__tests__/auto-merge.perf.test.d.ts.map +1 -0
  183. package/dist/team/__tests__/auto-merge.perf.test.js +85 -0
  184. package/dist/team/__tests__/auto-merge.perf.test.js.map +1 -0
  185. package/dist/team/__tests__/conflict-mailbox.test.d.ts +2 -0
  186. package/dist/team/__tests__/conflict-mailbox.test.d.ts.map +1 -0
  187. package/dist/team/__tests__/conflict-mailbox.test.js +238 -0
  188. package/dist/team/__tests__/conflict-mailbox.test.js.map +1 -0
  189. package/dist/team/__tests__/helpers/git-fixture.d.ts +72 -0
  190. package/dist/team/__tests__/helpers/git-fixture.d.ts.map +1 -0
  191. package/dist/team/__tests__/helpers/git-fixture.js +252 -0
  192. package/dist/team/__tests__/helpers/git-fixture.js.map +1 -0
  193. package/dist/team/__tests__/leader-inbox.test.d.ts +2 -0
  194. package/dist/team/__tests__/leader-inbox.test.d.ts.map +1 -0
  195. package/dist/team/__tests__/leader-inbox.test.js +152 -0
  196. package/dist/team/__tests__/leader-inbox.test.js.map +1 -0
  197. package/dist/team/__tests__/merge-orchestrator.test.d.ts +2 -0
  198. package/dist/team/__tests__/merge-orchestrator.test.d.ts.map +1 -0
  199. package/dist/team/__tests__/merge-orchestrator.test.js +779 -0
  200. package/dist/team/__tests__/merge-orchestrator.test.js.map +1 -0
  201. package/dist/team/__tests__/rebase-smoke.test.d.ts +2 -0
  202. package/dist/team/__tests__/rebase-smoke.test.d.ts.map +1 -0
  203. package/dist/team/__tests__/rebase-smoke.test.js +272 -0
  204. package/dist/team/__tests__/rebase-smoke.test.js.map +1 -0
  205. package/dist/team/__tests__/runtime-cli.test.js +9 -1
  206. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  207. package/dist/team/__tests__/runtime-v2.dispatch.test.js +150 -1
  208. package/dist/team/__tests__/runtime-v2.dispatch.test.js.map +1 -1
  209. package/dist/team/__tests__/teardown-invariant.test.d.ts +2 -0
  210. package/dist/team/__tests__/teardown-invariant.test.d.ts.map +1 -0
  211. package/dist/team/__tests__/teardown-invariant.test.js +176 -0
  212. package/dist/team/__tests__/teardown-invariant.test.js.map +1 -0
  213. package/dist/team/__tests__/worker-commit-cadence.test.d.ts +2 -0
  214. package/dist/team/__tests__/worker-commit-cadence.test.d.ts.map +1 -0
  215. package/dist/team/__tests__/worker-commit-cadence.test.js +315 -0
  216. package/dist/team/__tests__/worker-commit-cadence.test.js.map +1 -0
  217. package/dist/team/__tests__/worktree-runtime-e2e.test.d.ts +2 -0
  218. package/dist/team/__tests__/worktree-runtime-e2e.test.d.ts.map +1 -0
  219. package/dist/team/__tests__/worktree-runtime-e2e.test.js +182 -0
  220. package/dist/team/__tests__/worktree-runtime-e2e.test.js.map +1 -0
  221. package/dist/team/conflict-mailbox.d.ts +49 -0
  222. package/dist/team/conflict-mailbox.d.ts.map +1 -0
  223. package/dist/team/conflict-mailbox.js +98 -0
  224. package/dist/team/conflict-mailbox.js.map +1 -0
  225. package/dist/team/leader-inbox.d.ts +28 -0
  226. package/dist/team/leader-inbox.d.ts.map +1 -0
  227. package/dist/team/leader-inbox.js +68 -0
  228. package/dist/team/leader-inbox.js.map +1 -0
  229. package/dist/team/merge-coordinator.d.ts +6 -0
  230. package/dist/team/merge-coordinator.d.ts.map +1 -1
  231. package/dist/team/merge-coordinator.js +6 -2
  232. package/dist/team/merge-coordinator.js.map +1 -1
  233. package/dist/team/merge-orchestrator.d.ts +41 -0
  234. package/dist/team/merge-orchestrator.d.ts.map +1 -0
  235. package/dist/team/merge-orchestrator.js +736 -0
  236. package/dist/team/merge-orchestrator.js.map +1 -0
  237. package/dist/team/runtime-cli.d.ts +1 -0
  238. package/dist/team/runtime-cli.d.ts.map +1 -1
  239. package/dist/team/runtime-cli.js +15 -2
  240. package/dist/team/runtime-cli.js.map +1 -1
  241. package/dist/team/runtime-flags.d.ts +5 -0
  242. package/dist/team/runtime-flags.d.ts.map +1 -0
  243. package/dist/team/runtime-flags.js +12 -0
  244. package/dist/team/runtime-flags.js.map +1 -0
  245. package/dist/team/runtime-v2.d.ts +7 -1
  246. package/dist/team/runtime-v2.d.ts.map +1 -1
  247. package/dist/team/runtime-v2.js +185 -8
  248. package/dist/team/runtime-v2.js.map +1 -1
  249. package/dist/team/worker-bootstrap.d.ts +4 -0
  250. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  251. package/dist/team/worker-bootstrap.js +10 -1
  252. package/dist/team/worker-bootstrap.js.map +1 -1
  253. package/dist/team/worker-commit-cadence.d.ts +70 -0
  254. package/dist/team/worker-commit-cadence.d.ts.map +1 -0
  255. package/dist/team/worker-commit-cadence.js +271 -0
  256. package/dist/team/worker-commit-cadence.js.map +1 -0
  257. package/dist/tools/__tests__/memory-tools.test.js +4 -4
  258. package/dist/tools/__tests__/memory-tools.test.js.map +1 -1
  259. package/dist/tools/__tests__/state-tools.test.js +13 -1
  260. package/dist/tools/__tests__/state-tools.test.js.map +1 -1
  261. package/docs/GETTING-STARTED.md +415 -0
  262. package/docs/MIGRATION.md +66 -66
  263. package/docs/REFERENCE.md +21 -4
  264. package/docs/company-context-interface.md +5 -16
  265. package/docs/copilot-instructions.md +1 -1
  266. package/package.json +1 -1
  267. package/scripts/lib/hud-cache-wrapper.sh +122 -0
  268. package/scripts/plugin-setup.mjs +10 -1
  269. package/scripts/post-tool-verifier.mjs +118 -5
  270. package/scripts/pre-tool-enforcer.mjs +54 -6
  271. package/scripts/session-start.mjs +171 -3
  272. package/skills/AGENTS.md +22 -91
  273. package/skills/ai-slop-cleaner/SKILL.md +118 -120
  274. package/skills/ask/SKILL.md +6 -8
  275. package/skills/autopilot/SKILL.md +1 -28
  276. package/skills/autoresearch/SKILL.md +5 -5
  277. package/skills/cancel/SKILL.md +13 -13
  278. package/skills/cccg/SKILL.md +1 -0
  279. package/skills/configure-notifications/SKILL.md +80 -215
  280. package/skills/debug/SKILL.md +3 -2
  281. package/skills/deep-dive/SKILL.md +20 -9
  282. package/skills/deep-interview/SKILL.md +32 -4
  283. package/skills/deepinit/SKILL.md +1 -0
  284. package/skills/external-context/SKILL.md +2 -1
  285. package/skills/hud/SKILL.md +22 -52
  286. package/skills/learner/SKILL.md +90 -57
  287. package/skills/mcp-setup/SKILL.md +45 -60
  288. package/skills/omc-doctor/SKILL.md +1 -1
  289. package/skills/omc-reference/SKILL.md +6 -6
  290. package/skills/omc-setup/SKILL.md +30 -74
  291. package/skills/omc-setup/phases/01-install-claude-md.md +97 -0
  292. package/skills/omc-setup/phases/02-configure.md +25 -40
  293. package/skills/omc-setup/phases/03-integrations.md +100 -69
  294. package/skills/omc-setup/phases/04-welcome.md +102 -44
  295. package/skills/omc-teams/SKILL.md +63 -26
  296. package/skills/plan/SKILL.md +11 -9
  297. package/skills/ralph/SKILL.md +42 -41
  298. package/skills/ralplan/SKILL.md +3 -10
  299. package/skills/release/SKILL.md +168 -57
  300. package/skills/remember/SKILL.md +2 -1
  301. package/skills/sciomc/SKILL.md +12 -11
  302. package/skills/self-improve/SKILL.md +4 -4
  303. package/skills/setup/SKILL.md +1 -0
  304. package/skills/skill/SKILL.md +21 -20
  305. package/skills/skillify/SKILL.md +34 -3
  306. package/skills/team/SKILL.md +62 -44
  307. package/skills/ultraqa/SKILL.md +1 -2
  308. package/skills/ultrawork/SKILL.md +28 -21
  309. package/skills/verify/SKILL.md +2 -1
  310. package/skills/writer-memory/SKILL.md +2 -1
  311. package/templates/hooks/pre-tool-use.mjs +144 -45
@@ -0,0 +1,736 @@
1
+ // src/team/merge-orchestrator.ts
2
+ //
3
+ // Auto-merge / auto-rebase orchestrator for v2 team runtime.
4
+ //
5
+ // Responsibilities:
6
+ // - Watch each registered worker branch HEAD (commit watcher).
7
+ // - On observed SHA change: drive auto-merge from a dedicated "merger" worktree
8
+ // into the leader branch (mutex-serialised, with pre-flight reset).
9
+ // - On clean merge: fan-out auto-rebase to every other registered worker, with
10
+ // the M1 existing-rebase short-circuit, sentinel pause/resume, and M4 dirty
11
+ // audit on resolution.
12
+ // - Persist last-observed SHAs atomically so we can resume across restarts.
13
+ // - On stop: drain pending merges within drainTimeoutMs and emit a teardown
14
+ // audit row + leader-inbox audit message for any worker we couldn't merge.
15
+ //
16
+ // Hardening flags wired in:
17
+ // M1: existing rebase short-circuit (gitdir-aware `rebase-merge`).
18
+ // M3: refuse main/master leader branch.
19
+ // M4: dirty-tree audit on rebase resolution.
20
+ // M5: v2 gate via default-on runtime-v2 flag semantics.
21
+ // M6: persisted SHA state + restart recovery.
22
+ //
23
+ // All git invocations go through `execFileSync` (sync) so vitest can mock the
24
+ // node:child_process module via `vi.mock(...)` without spawning real processes.
25
+ //
26
+ // ## Plan divergences
27
+ //
28
+ // The implementation diverges from the original design plan in three places.
29
+ // These are intentional and known; flagged here so future readers don't try to
30
+ // "fix" them by aligning with the plan:
31
+ //
32
+ // 1. Events are written to a dedicated `orchestrator-events.jsonl` log under
33
+ // `.omcp/state/team/{team}/`, NOT to the shared `events.jsonl` used by the
34
+ // rest of the runtime. This keeps orchestrator-internal state isolated and
35
+ // avoids interleaving with worker/leader event streams that have different
36
+ // consumers and retention rules.
37
+ //
38
+ // 2. The leader inbox is written on conflict (merge or rebase) and on
39
+ // teardown (drain audit) only — NOT on every merge success. Successful
40
+ // merges are recorded in `orchestrator-events.jsonl` and don't need to
41
+ // page the leader. This keeps the leader inbox actionable instead of a
42
+ // noisy log.
43
+ //
44
+ // 3. `extendLeaderBootstrapPrompt` produces a directive that is appended to
45
+ // the leader inbox file at startup; it is NOT injected into a leader-pane
46
+ // spawn prompt because v2 has no leader-pane prompt hook. The v2 leader
47
+ // runs in the parent Claude Code session and discovers the inbox via the
48
+ // file path that gets written into the leader inbox itself.
49
+ //
50
+ // 4. Sentinel forgeability: `.hook-paused` is a plain file in the worktree
51
+ // root that any process inside the worktree can create or remove. This is
52
+ // a deliberate trade-off — workers and the orchestrator both need
53
+ // synchronous, lock-free coordination, and the worktree is already a
54
+ // trust boundary owned by the worker process. A worker that forges or
55
+ // deletes the sentinel is already inside the trust boundary.
56
+ import { execFileSync } from 'node:child_process';
57
+ import { existsSync } from 'node:fs';
58
+ import { mkdir, appendFile } from 'node:fs/promises';
59
+ import { dirname, join } from 'node:path';
60
+ import { atomicWriteJson, ensureDirWithMode, validateResolvedPath } from './fs-utils.js';
61
+ import { isRuntimeV2Enabled } from './runtime-flags.js';
62
+ import { sanitizeName } from './tmux-session.js';
63
+ import { listTeamWorktrees, getWorktreePath, getBranchName } from './git-worktree.js';
64
+ import { checkMergeConflicts, mergeWorkerBranch, validateBranchName } from './merge-coordinator.js';
65
+ import { appendToInbox } from './worker-bootstrap.js';
66
+ import { appendToLeaderInbox, ensureLeaderInbox } from './leader-inbox.js';
67
+ import { formatMergeConflictForLeader, formatRebaseConflictForWorker, } from './conflict-mailbox.js';
68
+ import { pauseHookViaSentinel, resumeHookViaSentinel, } from './worker-commit-cadence.js';
69
+ const DEFAULT_POLL_INTERVAL_MS = 1000;
70
+ const DEFAULT_DRAIN_TIMEOUT_MS = 10000;
71
+ // ---------------------------------------------------------------------------
72
+ // Path helpers
73
+ // ---------------------------------------------------------------------------
74
+ function mergerWorktreePathFor(repoRoot, teamName) {
75
+ return join(repoRoot, '.omcp', 'team', sanitizeName(teamName), 'merger');
76
+ }
77
+ function persistedStatePath(repoRoot, teamName) {
78
+ return join(repoRoot, '.omcp', 'state', 'team', sanitizeName(teamName), 'auto-merge-state.json');
79
+ }
80
+ function teardownAuditPath(repoRoot, teamName) {
81
+ return join(repoRoot, '.omcp', 'state', 'team', sanitizeName(teamName), 'teardown-audit.jsonl');
82
+ }
83
+ function orchestratorEventLogPath(repoRoot, teamName) {
84
+ return join(repoRoot, '.omcp', 'state', 'team', sanitizeName(teamName), 'orchestrator-events.jsonl');
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Guards (M3, M5)
88
+ // ---------------------------------------------------------------------------
89
+ function assertLeaderBranchAllowed(leaderBranch) {
90
+ const stripped = leaderBranch.replace(/^refs\/heads\//i, '').toLowerCase();
91
+ if (stripped === 'main' || stripped === 'master') {
92
+ throw new Error('auto-merge refuses main/master leader branch — use a feature branch');
93
+ }
94
+ }
95
+ function assertRuntimeV2Gate() {
96
+ if (!isRuntimeV2Enabled()) {
97
+ throw new Error('auto-merge requires runtime v2 (OMC_RUNTIME_V2 is explicitly disabled).');
98
+ }
99
+ }
100
+ async function appendEvent(repoRoot, teamName, event) {
101
+ const path = orchestratorEventLogPath(repoRoot, teamName);
102
+ await mkdir(dirname(path), { recursive: true });
103
+ const full = {
104
+ ts: new Date().toISOString(),
105
+ team: teamName,
106
+ ...event,
107
+ };
108
+ await appendFile(path, `${JSON.stringify(full)}\n`, 'utf-8');
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // Mutex (chained-Promise serialisation)
112
+ // ---------------------------------------------------------------------------
113
+ function createMutex() {
114
+ let lock = Promise.resolve();
115
+ return (fn) => {
116
+ const next = lock.then(fn, fn);
117
+ // Swallow rejections in the chain so a failed merge doesn't poison the lock.
118
+ lock = next.catch(() => undefined);
119
+ return next;
120
+ };
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Git helpers (all sync; mockable via vi.mock('node:child_process'))
124
+ // ---------------------------------------------------------------------------
125
+ function gitRevParseHead(repoRoot, branch) {
126
+ return execFileSync('git', ['rev-parse', `refs/heads/${branch}`], {
127
+ cwd: repoRoot,
128
+ encoding: 'utf-8',
129
+ stdio: 'pipe',
130
+ }).trim();
131
+ }
132
+ function gitPath(worktreePath, gitPathName) {
133
+ try {
134
+ const resolved = execFileSync('git', ['rev-parse', '--git-path', gitPathName], {
135
+ cwd: worktreePath,
136
+ encoding: 'utf-8',
137
+ stdio: 'pipe',
138
+ }).trim();
139
+ if (resolved)
140
+ return resolved;
141
+ }
142
+ catch {
143
+ // Fall through to the legacy in-worktree .git directory path. This keeps
144
+ // unit tests and non-git placeholder worktrees safe while real git
145
+ // worktrees use Git's own gitdir-aware resolution above.
146
+ }
147
+ return join(worktreePath, '.git', gitPathName);
148
+ }
149
+ function isRebaseInProgress(worktreePath) {
150
+ return existsSync(gitPath(worktreePath, 'rebase-merge'));
151
+ }
152
+ function isWorktreeRegistered(repoRoot, wtPath) {
153
+ try {
154
+ const out = execFileSync('git', ['worktree', 'list', '--porcelain'], {
155
+ cwd: repoRoot,
156
+ encoding: 'utf-8',
157
+ stdio: 'pipe',
158
+ });
159
+ for (const line of out.split('\n')) {
160
+ if (line.startsWith('worktree ')) {
161
+ if (line.slice('worktree '.length).trim() === wtPath)
162
+ return true;
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // best-effort
168
+ }
169
+ return false;
170
+ }
171
+ function ensureMergerWorktree(repoRoot, mergerPath, leaderBranch) {
172
+ ensureDirWithMode(dirname(mergerPath));
173
+ if (existsSync(mergerPath) && isWorktreeRegistered(repoRoot, mergerPath)) {
174
+ return; // reuse
175
+ }
176
+ // First-time create — allow the hidden merger worktree to check out the
177
+ // leader branch even when the normal leader repo already has it checked out.
178
+ execFileSync('git', ['worktree', 'add', '--force', mergerPath, leaderBranch], {
179
+ cwd: repoRoot,
180
+ stdio: 'pipe',
181
+ });
182
+ }
183
+ function preflightMergerWorktree(mergerPath, leaderBranch) {
184
+ // Best-effort fetch (ignore failures — offline OK).
185
+ try {
186
+ execFileSync('git', ['fetch', '--no-tags', 'origin', leaderBranch], {
187
+ cwd: mergerPath,
188
+ stdio: 'pipe',
189
+ });
190
+ }
191
+ catch {
192
+ // ignore
193
+ }
194
+ execFileSync('git', ['reset', '--hard', leaderBranch], {
195
+ cwd: mergerPath,
196
+ stdio: 'pipe',
197
+ });
198
+ }
199
+ function parseUUFiles(porcelainOutput) {
200
+ const files = [];
201
+ for (const line of porcelainOutput.split('\n')) {
202
+ if (line.startsWith('UU ')) {
203
+ files.push(line.slice(3).trim());
204
+ }
205
+ else if (line.startsWith('AA ') || line.startsWith('DD ')) {
206
+ // Both-modified variants count as conflicts too.
207
+ files.push(line.slice(3).trim());
208
+ }
209
+ }
210
+ return files;
211
+ }
212
+ // ---------------------------------------------------------------------------
213
+ // startMergeOrchestrator
214
+ // ---------------------------------------------------------------------------
215
+ export async function startMergeOrchestrator(config) {
216
+ assertRuntimeV2Gate();
217
+ assertLeaderBranchAllowed(config.leaderBranch);
218
+ // Refuse leader branch names that could be misread as flags by git
219
+ // (e.g. `--upload-pack=evil`, `-e/payload`).
220
+ validateBranchName(config.leaderBranch);
221
+ const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
222
+ const drainTimeoutMs = config.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
223
+ const mergerPath = mergerWorktreePathFor(config.repoRoot, config.teamName);
224
+ // Validate paths stay under repoRoot (defence-in-depth).
225
+ validateResolvedPath(mergerPath, config.repoRoot);
226
+ // Bootstrap merger worktree + leader inbox.
227
+ ensureMergerWorktree(config.repoRoot, mergerPath, config.leaderBranch);
228
+ await ensureLeaderInbox(config.teamName, config.cwd);
229
+ // Hydrate from persisted state if present (M6).
230
+ const persistedPath = persistedStatePath(config.repoRoot, config.teamName);
231
+ let persisted = { lastShas: {} };
232
+ if (existsSync(persistedPath)) {
233
+ try {
234
+ const { readFileSync } = await import('node:fs');
235
+ persisted = JSON.parse(readFileSync(persistedPath, 'utf-8'));
236
+ }
237
+ catch {
238
+ persisted = { lastShas: {} };
239
+ }
240
+ }
241
+ const workers = new Map();
242
+ const pausedWorkers = new Set(); // workers mid-rebase (cadence paused)
243
+ const mutex = createMutex();
244
+ let stopped = false;
245
+ function persistState() {
246
+ const payload = {
247
+ lastShas: Object.fromEntries(Array.from(workers.values()).map((w) => [w.workerName, w.lastObservedSha])),
248
+ };
249
+ atomicWriteJson(persistedPath, payload);
250
+ }
251
+ async function fanOutRebase(triggeringWorker) {
252
+ for (const other of workers.values()) {
253
+ if (other.workerName === triggeringWorker)
254
+ continue;
255
+ const wtPath = other.workerWorktreePath;
256
+ // M1: existing rebase short-circuit.
257
+ if (isRebaseInProgress(wtPath)) {
258
+ await appendEvent(config.repoRoot, config.teamName, {
259
+ type: 'rebase_skipped_in_progress',
260
+ worker: other.workerName,
261
+ reason: 'rebase-already-in-progress',
262
+ });
263
+ continue;
264
+ }
265
+ await appendEvent(config.repoRoot, config.teamName, {
266
+ type: 'rebase_triggered',
267
+ worker: other.workerName,
268
+ });
269
+ // Pause cadence before touching the worktree.
270
+ await pauseHookViaSentinel(wtPath);
271
+ pausedWorkers.add(other.workerName);
272
+ // Best-effort fetch.
273
+ try {
274
+ execFileSync('git', ['fetch', '--no-tags', 'origin', config.leaderBranch], {
275
+ cwd: wtPath,
276
+ stdio: 'pipe',
277
+ });
278
+ }
279
+ catch {
280
+ // offline OK
281
+ }
282
+ try {
283
+ execFileSync('git', ['rebase', config.leaderBranch], {
284
+ cwd: wtPath,
285
+ stdio: 'pipe',
286
+ });
287
+ // Clean rebase — resume immediately.
288
+ await resumeHookViaSentinel(wtPath);
289
+ pausedWorkers.delete(other.workerName);
290
+ await appendEvent(config.repoRoot, config.teamName, {
291
+ type: 'rebase_succeeded',
292
+ worker: other.workerName,
293
+ });
294
+ }
295
+ catch {
296
+ // Conflict — leave worktree mid-rebase, parse UU files, deliver mailbox.
297
+ let conflictingFiles = [];
298
+ try {
299
+ const status = execFileSync('git', ['status', '--porcelain'], {
300
+ cwd: wtPath,
301
+ encoding: 'utf-8',
302
+ stdio: 'pipe',
303
+ });
304
+ conflictingFiles = parseUUFiles(status);
305
+ }
306
+ catch {
307
+ conflictingFiles = ['(rebase status unavailable)'];
308
+ }
309
+ const baseSha = (() => {
310
+ try {
311
+ return execFileSync('git', ['rev-parse', `refs/heads/${config.leaderBranch}`], {
312
+ cwd: config.repoRoot,
313
+ encoding: 'utf-8',
314
+ stdio: 'pipe',
315
+ }).trim();
316
+ }
317
+ catch {
318
+ return 'unknown';
319
+ }
320
+ })();
321
+ const message = formatRebaseConflictForWorker({
322
+ workerName: other.workerName,
323
+ workerBranch: other.workerBranch,
324
+ leaderBranch: config.leaderBranch,
325
+ conflictingFiles,
326
+ baseSha,
327
+ worktreePath: wtPath,
328
+ observedAt: Date.now(),
329
+ });
330
+ try {
331
+ await appendToInbox(config.teamName, other.workerName, message, config.cwd);
332
+ }
333
+ catch {
334
+ // best-effort — don't crash poller on inbox write failure
335
+ }
336
+ await appendEvent(config.repoRoot, config.teamName, {
337
+ type: 'rebase_conflict',
338
+ worker: other.workerName,
339
+ data: { conflictingFiles },
340
+ });
341
+ // Cadence stays paused. The rebase resolution watcher resumes on
342
+ // git rebase state removal (M4 audit, then resume).
343
+ }
344
+ }
345
+ }
346
+ async function attemptMergeForWorker(entry) {
347
+ await mutex(async () => {
348
+ // Snapshot the SHA at the time the lock is taken — this is the merge target.
349
+ const targetSha = entry.lastObservedSha;
350
+ await appendEvent(config.repoRoot, config.teamName, {
351
+ type: 'merge_attempted',
352
+ worker: entry.workerName,
353
+ data: { targetSha },
354
+ });
355
+ try {
356
+ preflightMergerWorktree(mergerPath, config.leaderBranch);
357
+ }
358
+ catch (err) {
359
+ const reason = err instanceof Error ? err.message : String(err);
360
+ entry.consecutiveFailures += 1;
361
+ await appendEvent(config.repoRoot, config.teamName, {
362
+ type: 'merge_conflict',
363
+ worker: entry.workerName,
364
+ reason: `preflight_failed:${reason}`,
365
+ });
366
+ return;
367
+ }
368
+ const conflicts = checkMergeConflicts(entry.workerBranch, config.leaderBranch, mergerPath);
369
+ if (conflicts.length > 0) {
370
+ // Deliver merge-conflict mailbox to leader.
371
+ let mergeBaseSha = 'unknown';
372
+ try {
373
+ mergeBaseSha = execFileSync('git', ['merge-base', config.leaderBranch, entry.workerBranch], { cwd: mergerPath, encoding: 'utf-8', stdio: 'pipe' }).trim();
374
+ }
375
+ catch {
376
+ // best-effort
377
+ }
378
+ const message = formatMergeConflictForLeader({
379
+ workerName: entry.workerName,
380
+ workerBranch: entry.workerBranch,
381
+ leaderBranch: config.leaderBranch,
382
+ conflictingFiles: conflicts,
383
+ mergeBaseSha,
384
+ observedAt: Date.now(),
385
+ });
386
+ try {
387
+ await appendToLeaderInbox(config.teamName, message, config.cwd);
388
+ }
389
+ catch {
390
+ // best-effort
391
+ }
392
+ await appendEvent(config.repoRoot, config.teamName, {
393
+ type: 'merge_conflict',
394
+ worker: entry.workerName,
395
+ data: { conflictingFiles: conflicts, mergeBaseSha },
396
+ });
397
+ entry.consecutiveFailures += 1;
398
+ return;
399
+ }
400
+ // Clean — execute the merge in the merger worktree.
401
+ const result = mergeWorkerBranch(entry.workerBranch, config.leaderBranch, mergerPath);
402
+ if (!result.success) {
403
+ // Race: conflict-detection passed but the actual merge failed (e.g. a new
404
+ // commit landed). Fall back to the conflict-mailbox path.
405
+ const message = formatMergeConflictForLeader({
406
+ workerName: entry.workerName,
407
+ workerBranch: entry.workerBranch,
408
+ leaderBranch: config.leaderBranch,
409
+ conflictingFiles: result.conflicts.length > 0 ? result.conflicts : ['(merge failed after clean check)'],
410
+ mergeBaseSha: 'unknown',
411
+ observedAt: Date.now(),
412
+ });
413
+ try {
414
+ await appendToLeaderInbox(config.teamName, message, config.cwd);
415
+ }
416
+ catch {
417
+ // best-effort
418
+ }
419
+ await appendEvent(config.repoRoot, config.teamName, {
420
+ type: 'merge_conflict',
421
+ worker: entry.workerName,
422
+ data: { conflictingFiles: result.conflicts },
423
+ });
424
+ entry.consecutiveFailures += 1;
425
+ return;
426
+ }
427
+ entry.lastMergedSha = targetSha;
428
+ entry.consecutiveFailures = 0;
429
+ await appendEvent(config.repoRoot, config.teamName, {
430
+ type: 'merge_succeeded',
431
+ worker: entry.workerName,
432
+ data: { mergeCommit: result.mergeCommit, targetSha },
433
+ });
434
+ // Clean merge → fan out rebase to other workers.
435
+ // Skip fan-out after `stopped` is set so a final drain merge cannot race
436
+ // with `cleanupTeamWorktrees` reaching into other workers' worktrees.
437
+ if (stopped)
438
+ return;
439
+ await fanOutRebase(entry.workerName);
440
+ });
441
+ }
442
+ async function runPollOnce() {
443
+ if (stopped)
444
+ return;
445
+ for (const entry of workers.values()) {
446
+ // Apply per-worker exponential backoff: skip ticks based on consecutiveFailures.
447
+ // Cap so we never fully starve a worker.
448
+ const skipModulo = Math.min(30, Math.pow(2, entry.consecutiveFailures));
449
+ if (skipModulo > 1 && pollTickCount % skipModulo !== 0) {
450
+ continue;
451
+ }
452
+ // Rebase resolution check (M4): if the worker is paused and the
453
+ // git rebase state has gone away, audit & resume.
454
+ if (pausedWorkers.has(entry.workerName)) {
455
+ if (!isRebaseInProgress(entry.workerWorktreePath)) {
456
+ // Resolved (--continue or --abort).
457
+ await handleRebaseResolution(entry);
458
+ // Fall through — also check for new commits.
459
+ }
460
+ else {
461
+ // Still mid-rebase; don't try to merge this worker's branch yet.
462
+ continue;
463
+ }
464
+ }
465
+ let currentSha;
466
+ try {
467
+ currentSha = gitRevParseHead(config.repoRoot, entry.workerBranch);
468
+ }
469
+ catch (err) {
470
+ entry.consecutiveFailures += 1;
471
+ const reason = err instanceof Error ? err.message : String(err);
472
+ await appendEvent(config.repoRoot, config.teamName, {
473
+ type: 'commit_observed',
474
+ worker: entry.workerName,
475
+ reason: `rev_parse_failed:${reason}`,
476
+ });
477
+ continue;
478
+ }
479
+ if (currentSha && currentSha !== entry.lastObservedSha) {
480
+ // SHA advanced → record + persist + try to merge.
481
+ entry.lastObservedSha = currentSha;
482
+ try {
483
+ persistState();
484
+ }
485
+ catch {
486
+ // best-effort persistence
487
+ }
488
+ await appendEvent(config.repoRoot, config.teamName, {
489
+ type: 'commit_observed',
490
+ worker: entry.workerName,
491
+ data: { sha: currentSha },
492
+ });
493
+ try {
494
+ await attemptMergeForWorker(entry);
495
+ }
496
+ catch (err) {
497
+ entry.consecutiveFailures += 1;
498
+ const reason = err instanceof Error ? err.message : String(err);
499
+ await appendEvent(config.repoRoot, config.teamName, {
500
+ type: 'merge_conflict',
501
+ worker: entry.workerName,
502
+ reason: `merge_threw:${reason}`,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ }
508
+ async function handleRebaseResolution(entry) {
509
+ pausedWorkers.delete(entry.workerName);
510
+ // M4: dirty-tree audit before resuming cadence.
511
+ try {
512
+ const status = execFileSync('git', ['status', '--porcelain'], {
513
+ cwd: entry.workerWorktreePath,
514
+ encoding: 'utf-8',
515
+ stdio: 'pipe',
516
+ }).trim();
517
+ if (status.length > 0) {
518
+ const dirtyFiles = status
519
+ .split('\n')
520
+ .map((l) => l.trim().replace(/^\S+\s+/, ''))
521
+ .filter((s) => s.length > 0);
522
+ const audit = `## Auto-commit audit: the following files were modified during rebase pause and will be folded into the next auto-commit:\n${dirtyFiles.map((f) => `- \`${f}\``).join('\n')}`;
523
+ try {
524
+ await appendToInbox(config.teamName, entry.workerName, audit, config.cwd);
525
+ }
526
+ catch {
527
+ // best-effort
528
+ }
529
+ }
530
+ }
531
+ catch {
532
+ // status failed — proceed without audit
533
+ }
534
+ await resumeHookViaSentinel(entry.workerWorktreePath);
535
+ await appendEvent(config.repoRoot, config.teamName, {
536
+ type: 'rebase_resolved',
537
+ worker: entry.workerName,
538
+ });
539
+ }
540
+ // ----- Polling driver -----
541
+ let pollTickCount = 0;
542
+ const interval = setInterval(() => {
543
+ pollTickCount += 1;
544
+ void runPollOnce().catch(() => {
545
+ // Errors bubble up here as a defence; per-worker failures already
546
+ // emit events and bump consecutiveFailures inside pollOnce().
547
+ });
548
+ }, pollIntervalMs);
549
+ // Don't keep the event loop alive on its own.
550
+ if (typeof interval.unref === 'function')
551
+ interval.unref();
552
+ // ----- Public handle -----
553
+ return {
554
+ async registerWorker(workerName) {
555
+ if (workers.has(workerName))
556
+ return;
557
+ const workerBranch = getBranchName(config.teamName, workerName);
558
+ // Defence-in-depth: reject worker branch names that could be misread as
559
+ // flags by git (e.g. `--upload-pack=foo`). `getBranchName` is supposed to
560
+ // produce safe names, but we re-validate here so any future change to
561
+ // the naming scheme can't accidentally bypass this guard.
562
+ validateBranchName(workerBranch);
563
+ const wtPath = getWorktreePath(config.repoRoot, config.teamName, workerName);
564
+ let seedSha = persisted.lastShas[workerName] ?? '';
565
+ if (!seedSha) {
566
+ try {
567
+ seedSha = gitRevParseHead(config.repoRoot, workerBranch);
568
+ }
569
+ catch {
570
+ seedSha = '';
571
+ }
572
+ }
573
+ workers.set(workerName, {
574
+ workerName,
575
+ workerBranch,
576
+ workerWorktreePath: wtPath,
577
+ lastObservedSha: seedSha,
578
+ lastMergedSha: seedSha,
579
+ consecutiveFailures: 0,
580
+ });
581
+ try {
582
+ persistState();
583
+ }
584
+ catch {
585
+ // best-effort
586
+ }
587
+ },
588
+ async unregisterWorker(workerName) {
589
+ workers.delete(workerName);
590
+ pausedWorkers.delete(workerName);
591
+ try {
592
+ persistState();
593
+ }
594
+ catch {
595
+ // best-effort
596
+ }
597
+ },
598
+ async pollOnce() {
599
+ await runPollOnce();
600
+ },
601
+ async drainAndStop() {
602
+ stopped = true;
603
+ clearInterval(interval);
604
+ const start = Date.now();
605
+ const unmerged = [];
606
+ const candidates = Array.from(workers.values()).filter((w) => w.lastObservedSha && w.lastObservedSha !== w.lastMergedSha);
607
+ for (const entry of candidates) {
608
+ const remaining = drainTimeoutMs - (Date.now() - start);
609
+ if (remaining <= 0) {
610
+ unmerged.push({ workerName: entry.workerName, reason: 'drain-timeout' });
611
+ continue;
612
+ }
613
+ const merged = await Promise.race([
614
+ (async () => {
615
+ try {
616
+ await attemptMergeForWorker(entry);
617
+ return true;
618
+ }
619
+ catch {
620
+ return false;
621
+ }
622
+ })(),
623
+ new Promise((resolve) => {
624
+ const t = setTimeout(() => resolve(false), remaining);
625
+ if (typeof t.unref === 'function')
626
+ t.unref();
627
+ }),
628
+ ]);
629
+ if (!merged || entry.lastMergedSha !== entry.lastObservedSha) {
630
+ unmerged.push({
631
+ workerName: entry.workerName,
632
+ reason: merged ? 'merge-conflict' : 'drain-timeout',
633
+ });
634
+ }
635
+ }
636
+ // Audit any unmerged drain residue.
637
+ if (unmerged.length > 0) {
638
+ const auditPath = teardownAuditPath(config.repoRoot, config.teamName);
639
+ await mkdir(dirname(auditPath), { recursive: true });
640
+ for (const u of unmerged) {
641
+ const row = JSON.stringify({
642
+ type: 'unmerged_at_shutdown',
643
+ ts: new Date().toISOString(),
644
+ team: config.teamName,
645
+ worker: u.workerName,
646
+ reason: u.reason,
647
+ });
648
+ try {
649
+ await appendFile(auditPath, `${row}\n`, 'utf-8');
650
+ }
651
+ catch {
652
+ // best-effort
653
+ }
654
+ }
655
+ const message = `## Teardown audit: unmerged worker branches at shutdown\n\n${unmerged.map((u) => `- ${u.workerName}: ${u.reason}`).join('\n')}`;
656
+ try {
657
+ await appendToLeaderInbox(config.teamName, message, config.cwd);
658
+ }
659
+ catch {
660
+ // best-effort
661
+ }
662
+ }
663
+ return { unmerged };
664
+ },
665
+ getState() {
666
+ return {
667
+ workers: Array.from(workers.keys()),
668
+ lastShas: Object.fromEntries(Array.from(workers.values()).map((w) => [w.workerName, w.lastObservedSha])),
669
+ mergerWorktreePath: mergerPath,
670
+ };
671
+ },
672
+ };
673
+ }
674
+ // ---------------------------------------------------------------------------
675
+ // recoverFromRestart (M6)
676
+ // ---------------------------------------------------------------------------
677
+ export async function recoverFromRestart(config) {
678
+ // Load persisted SHA state.
679
+ const persistedPath = persistedStatePath(config.repoRoot, config.teamName);
680
+ let persistedShasLoaded = 0;
681
+ if (existsSync(persistedPath)) {
682
+ try {
683
+ const { readFileSync } = await import('node:fs');
684
+ const persisted = JSON.parse(readFileSync(persistedPath, 'utf-8'));
685
+ persistedShasLoaded = Object.keys(persisted.lastShas ?? {}).length;
686
+ }
687
+ catch {
688
+ persistedShasLoaded = 0;
689
+ }
690
+ }
691
+ // Walk every known worker worktree (from worktrees.json metadata) and look
692
+ // for orphaned gitdir-aware `rebase-merge` dirs.
693
+ const orphanedRebases = [];
694
+ let entries = [];
695
+ try {
696
+ entries = listTeamWorktrees(config.teamName, config.repoRoot).map((w) => ({
697
+ workerName: w.workerName,
698
+ path: w.path,
699
+ }));
700
+ }
701
+ catch {
702
+ entries = [];
703
+ }
704
+ for (const entry of entries) {
705
+ if (!isRebaseInProgress(entry.path))
706
+ continue;
707
+ orphanedRebases.push(entry.workerName);
708
+ const message = `### Runtime restart recovery — your branch is mid-rebase
709
+
710
+ Runtime restarted while your branch was mid-rebase onto \`${config.leaderBranch}\`.
711
+
712
+ **Worktree:** \`${entry.path}\`
713
+
714
+ Cadence remains paused. Resolve and \`git rebase --continue\`, or \`git rebase --abort\` to bail.
715
+ Cadence resumes once the git rebase state is gone.`;
716
+ try {
717
+ await appendToInbox(config.teamName, entry.workerName, message, config.cwd);
718
+ }
719
+ catch {
720
+ // best-effort
721
+ }
722
+ }
723
+ if (orphanedRebases.length > 0 || persistedShasLoaded > 0) {
724
+ try {
725
+ await appendEvent(config.repoRoot, config.teamName, {
726
+ type: 'restart_recovery',
727
+ data: { orphanedRebases, persistedShasLoaded },
728
+ });
729
+ }
730
+ catch {
731
+ // best-effort
732
+ }
733
+ }
734
+ return { orphanedRebases, persistedShasLoaded };
735
+ }
736
+ //# sourceMappingURL=merge-orchestrator.js.map