oh-my-codex 0.20.1 → 0.20.3

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 (791) hide show
  1. package/Cargo.lock +7 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +36 -1
  4. package/crates/omx-runtime/Cargo.toml +1 -0
  5. package/crates/omx-runtime/src/main.rs +25 -2
  6. package/crates/omx-runtime/tests/execution.rs +219 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/hermes.test.js.map +1 -1
  11. package/dist/adapt/contracts.d.ts.map +1 -1
  12. package/dist/agents/__tests__/definitions.test.js +81 -0
  13. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  14. package/dist/agents/__tests__/native-config.test.js +25 -0
  15. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  16. package/dist/agents/native-config.d.ts +2 -1
  17. package/dist/agents/native-config.d.ts.map +1 -1
  18. package/dist/agents/native-config.js.map +1 -1
  19. package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
  20. package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
  21. package/dist/auth/__tests__/hotswap.test.js +183 -0
  22. package/dist/auth/__tests__/hotswap.test.js.map +1 -0
  23. package/dist/auth/hotswap.d.ts +6 -0
  24. package/dist/auth/hotswap.d.ts.map +1 -1
  25. package/dist/auth/hotswap.js +102 -19
  26. package/dist/auth/hotswap.js.map +1 -1
  27. package/dist/auth/paths.js.map +1 -1
  28. package/dist/auth/storage.js.map +1 -1
  29. package/dist/autopilot/__tests__/ralplan-gate.test.js +5 -5
  30. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  31. package/dist/autopilot/completion-gate.js.map +1 -1
  32. package/dist/autopilot/fsm.d.ts +1 -1
  33. package/dist/autopilot/fsm.d.ts.map +1 -1
  34. package/dist/autoresearch/goal.js.map +1 -1
  35. package/dist/autoresearch/runtime.js.map +1 -1
  36. package/dist/capabilities/lockfile.js.map +1 -1
  37. package/dist/catalog/reader.js.map +1 -1
  38. package/dist/catalog/skill-mirror.js.map +1 -1
  39. package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
  40. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  41. package/dist/cli/__tests__/agents.test.js.map +1 -1
  42. package/dist/cli/__tests__/api.test.js.map +1 -1
  43. package/dist/cli/__tests__/ask.test.js.map +1 -1
  44. package/dist/cli/__tests__/auth.test.js +60 -6
  45. package/dist/cli/__tests__/auth.test.js.map +1 -1
  46. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  47. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  48. package/dist/cli/__tests__/capabilities.test.js.map +1 -1
  49. package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
  50. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  51. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  52. package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
  53. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  54. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  55. package/dist/cli/__tests__/doctor-warning-copy.test.js +752 -15
  56. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  57. package/dist/cli/__tests__/exec.test.js +136 -0
  58. package/dist/cli/__tests__/exec.test.js.map +1 -1
  59. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  60. package/dist/cli/__tests__/index.test.js +473 -14
  61. package/dist/cli/__tests__/index.test.js.map +1 -1
  62. package/dist/cli/__tests__/launch-fallback.test.js +336 -4
  63. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  64. package/dist/cli/__tests__/mcp-parity.test.js +87 -12
  65. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  66. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  67. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  68. package/dist/cli/__tests__/native-hook-claim-journal.test.js +206 -0
  69. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  70. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  71. package/dist/cli/__tests__/question.test.js.map +1 -1
  72. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  73. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  74. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  75. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  76. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  77. package/dist/cli/__tests__/ralph.test.js +8 -3
  78. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  79. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  80. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  81. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +72 -0
  82. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  83. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  84. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  85. package/dist/cli/__tests__/ralplan.test.js +55 -0
  86. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  87. package/dist/cli/__tests__/resume.test.js +17 -11
  88. package/dist/cli/__tests__/resume.test.js.map +1 -1
  89. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  90. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  91. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  92. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  93. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  94. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  95. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  96. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  97. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
  98. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  99. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  100. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  101. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  102. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  103. package/dist/cli/__tests__/setup-install-mode.test.js +2579 -294
  104. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  105. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  106. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  107. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  108. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  109. package/dist/cli/__tests__/setup-refresh.test.js +33 -2
  110. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  111. package/dist/cli/__tests__/setup-scope.test.js +46 -3
  112. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  113. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  114. package/dist/cli/__tests__/state.test.js +37 -0
  115. package/dist/cli/__tests__/state.test.js.map +1 -1
  116. package/dist/cli/__tests__/team.test.js +105 -7
  117. package/dist/cli/__tests__/team.test.js.map +1 -1
  118. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  119. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  120. package/dist/cli/__tests__/uninstall.test.js +1817 -24
  121. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  122. package/dist/cli/__tests__/update.test.js +58 -0
  123. package/dist/cli/__tests__/update.test.js.map +1 -1
  124. package/dist/cli/__tests__/url.test.js.map +1 -1
  125. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
  126. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  127. package/dist/cli/adapt.js.map +1 -1
  128. package/dist/cli/agents-init.js.map +1 -1
  129. package/dist/cli/agents.js.map +1 -1
  130. package/dist/cli/api.js.map +1 -1
  131. package/dist/cli/ask.d.ts +1 -1
  132. package/dist/cli/ask.d.ts.map +1 -1
  133. package/dist/cli/ask.js.map +1 -1
  134. package/dist/cli/auth.js.map +1 -1
  135. package/dist/cli/autoresearch-guided.js.map +1 -1
  136. package/dist/cli/autoresearch-intake.js.map +1 -1
  137. package/dist/cli/capabilities.js.map +1 -1
  138. package/dist/cli/cleanup.js.map +1 -1
  139. package/dist/cli/codex-feature-probe.js.map +1 -1
  140. package/dist/cli/codex-home.js.map +1 -1
  141. package/dist/cli/doctor.d.ts +21 -0
  142. package/dist/cli/doctor.d.ts.map +1 -1
  143. package/dist/cli/doctor.js +607 -130
  144. package/dist/cli/doctor.js.map +1 -1
  145. package/dist/cli/explore.js.map +1 -1
  146. package/dist/cli/index.d.ts +14 -3
  147. package/dist/cli/index.d.ts.map +1 -1
  148. package/dist/cli/index.js +245 -57
  149. package/dist/cli/index.js.map +1 -1
  150. package/dist/cli/mcp-parity.js +2 -2
  151. package/dist/cli/mcp-parity.js.map +1 -1
  152. package/dist/cli/mcp-serve.js.map +1 -1
  153. package/dist/cli/mission.js.map +1 -1
  154. package/dist/cli/native-assets.js.map +1 -1
  155. package/dist/cli/native-hook-claim-journal.d.ts +29 -0
  156. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  157. package/dist/cli/native-hook-claim-journal.js +252 -0
  158. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  159. package/dist/cli/omx.js.map +1 -1
  160. package/dist/cli/plugin-marketplace.js.map +1 -1
  161. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  162. package/dist/cli/question.js.map +1 -1
  163. package/dist/cli/ralph.d.ts.map +1 -1
  164. package/dist/cli/ralph.js +3 -2
  165. package/dist/cli/ralph.js.map +1 -1
  166. package/dist/cli/ralplan.d.ts +10 -0
  167. package/dist/cli/ralplan.d.ts.map +1 -0
  168. package/dist/cli/ralplan.js +110 -0
  169. package/dist/cli/ralplan.js.map +1 -0
  170. package/dist/cli/session-search.d.ts +5 -0
  171. package/dist/cli/session-search.d.ts.map +1 -1
  172. package/dist/cli/session-search.js +87 -0
  173. package/dist/cli/session-search.js.map +1 -1
  174. package/dist/cli/setup-preferences.d.ts +11 -0
  175. package/dist/cli/setup-preferences.d.ts.map +1 -1
  176. package/dist/cli/setup-preferences.js +32 -0
  177. package/dist/cli/setup-preferences.js.map +1 -1
  178. package/dist/cli/setup.d.ts +58 -0
  179. package/dist/cli/setup.d.ts.map +1 -1
  180. package/dist/cli/setup.js +1450 -398
  181. package/dist/cli/setup.js.map +1 -1
  182. package/dist/cli/sparkshell.js.map +1 -1
  183. package/dist/cli/star-prompt.js.map +1 -1
  184. package/dist/cli/state.d.ts.map +1 -1
  185. package/dist/cli/state.js +2 -1
  186. package/dist/cli/state.js.map +1 -1
  187. package/dist/cli/team.js.map +1 -1
  188. package/dist/cli/ultragoal.d.ts +5 -1
  189. package/dist/cli/ultragoal.d.ts.map +1 -1
  190. package/dist/cli/ultragoal.js +34 -2
  191. package/dist/cli/ultragoal.js.map +1 -1
  192. package/dist/cli/uninstall.d.ts +17 -0
  193. package/dist/cli/uninstall.d.ts.map +1 -1
  194. package/dist/cli/uninstall.js +986 -95
  195. package/dist/cli/uninstall.js.map +1 -1
  196. package/dist/cli/update.d.ts.map +1 -1
  197. package/dist/cli/update.js +8 -1
  198. package/dist/cli/update.js.map +1 -1
  199. package/dist/cli/url.js.map +1 -1
  200. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  201. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  202. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  203. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  204. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  205. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  206. package/dist/config/__tests__/generator-idempotent.test.js +1179 -32
  207. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  208. package/dist/config/__tests__/generator-notify.test.js +6 -1
  209. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  210. package/dist/config/__tests__/models.test.js +67 -3
  211. package/dist/config/__tests__/models.test.js.map +1 -1
  212. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  213. package/dist/config/codex-feature-flags.js.map +1 -1
  214. package/dist/config/codex-hooks.d.ts +124 -1
  215. package/dist/config/codex-hooks.d.ts.map +1 -1
  216. package/dist/config/codex-hooks.js +2142 -265
  217. package/dist/config/codex-hooks.js.map +1 -1
  218. package/dist/config/commit-lore-guard.js.map +1 -1
  219. package/dist/config/generator.d.ts +23 -34
  220. package/dist/config/generator.d.ts.map +1 -1
  221. package/dist/config/generator.js +1480 -246
  222. package/dist/config/generator.js.map +1 -1
  223. package/dist/config/mcp-registry.js.map +1 -1
  224. package/dist/config/models.d.ts +14 -4
  225. package/dist/config/models.d.ts.map +1 -1
  226. package/dist/config/models.js +15 -1
  227. package/dist/config/models.js.map +1 -1
  228. package/dist/config/omx-first-party-mcp.js.map +1 -1
  229. package/dist/config/team-mode.d.ts.map +1 -1
  230. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  231. package/dist/document-refresh/enforcer.js.map +1 -1
  232. package/dist/exec/followup.js.map +1 -1
  233. package/dist/goal-workflows/artifacts.js.map +1 -1
  234. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  235. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  236. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  237. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  238. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  239. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  240. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  241. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  242. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  243. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  244. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  245. package/dist/hooks/__tests__/keyword-detector.test.js +1376 -6
  246. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  247. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +252 -543
  248. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  249. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
  250. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  251. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +765 -10
  252. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  253. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  254. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  255. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  256. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  257. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  258. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  259. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  260. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  261. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  262. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  263. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  264. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  265. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  266. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  267. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
  268. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  269. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +163 -24
  270. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  271. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  272. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  273. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  274. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  275. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  276. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  277. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  278. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
  279. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  280. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  281. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  282. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  283. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  284. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  285. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  286. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  287. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  288. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  289. package/dist/hooks/__tests__/session.test.js +878 -11
  290. package/dist/hooks/__tests__/session.test.js.map +1 -1
  291. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  292. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  293. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  294. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  295. package/dist/hooks/agents-overlay.js +5 -5
  296. package/dist/hooks/agents-overlay.js.map +1 -1
  297. package/dist/hooks/code-simplifier/index.js.map +1 -1
  298. package/dist/hooks/explore-routing.js.map +1 -1
  299. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  300. package/dist/hooks/extensibility/events.js.map +1 -1
  301. package/dist/hooks/extensibility/loader.js.map +1 -1
  302. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  303. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  304. package/dist/hooks/keyword-detector.d.ts +33 -7
  305. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  306. package/dist/hooks/keyword-detector.js +2228 -119
  307. package/dist/hooks/keyword-detector.js.map +1 -1
  308. package/dist/hooks/keyword-registry.d.ts +12 -0
  309. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  310. package/dist/hooks/keyword-registry.js +41 -0
  311. package/dist/hooks/keyword-registry.js.map +1 -1
  312. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  313. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  314. package/dist/hooks/prompt-session-provenance.js +180 -0
  315. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  316. package/dist/hooks/session.d.ts +163 -34
  317. package/dist/hooks/session.d.ts.map +1 -1
  318. package/dist/hooks/session.js +1395 -317
  319. package/dist/hooks/session.js.map +1 -1
  320. package/dist/hooks/task-size-detector.js.map +1 -1
  321. package/dist/hud/__tests__/state.test.js.map +1 -1
  322. package/dist/hud/authority.js.map +1 -1
  323. package/dist/hud/constants.js.map +1 -1
  324. package/dist/hud/index.js.map +1 -1
  325. package/dist/hud/reconcile.js.map +1 -1
  326. package/dist/hud/render.js.map +1 -1
  327. package/dist/hud/state.js.map +1 -1
  328. package/dist/hud/tmux.js.map +1 -1
  329. package/dist/leader/__tests__/contract.test.js +153 -1
  330. package/dist/leader/__tests__/contract.test.js.map +1 -1
  331. package/dist/leader/contract.d.ts +42 -5
  332. package/dist/leader/contract.d.ts.map +1 -1
  333. package/dist/leader/contract.js +184 -3
  334. package/dist/leader/contract.js.map +1 -1
  335. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  336. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  337. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  338. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  339. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  340. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  341. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  342. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  343. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  344. package/dist/mcp/bootstrap.js.map +1 -1
  345. package/dist/mcp/code-intel-server.d.ts +58 -58
  346. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  347. package/dist/mcp/code-intel-server.js +13 -3
  348. package/dist/mcp/code-intel-server.js.map +1 -1
  349. package/dist/mcp/hermes-bridge.js.map +1 -1
  350. package/dist/mcp/hermes-server.d.ts +70 -70
  351. package/dist/mcp/hermes-server.d.ts.map +1 -1
  352. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  353. package/dist/mcp/memory-server.d.ts +34 -34
  354. package/dist/mcp/memory-server.d.ts.map +1 -1
  355. package/dist/mcp/memory-server.js.map +1 -1
  356. package/dist/mcp/state-paths.d.ts +14 -0
  357. package/dist/mcp/state-paths.d.ts.map +1 -1
  358. package/dist/mcp/state-paths.js +96 -20
  359. package/dist/mcp/state-paths.js.map +1 -1
  360. package/dist/mcp/state-server.d.ts +47 -47
  361. package/dist/mcp/state-server.d.ts.map +1 -1
  362. package/dist/mcp/trace-server.d.ts +4 -4
  363. package/dist/mcp/trace-server.d.ts.map +1 -1
  364. package/dist/mcp/trace-server.js.map +1 -1
  365. package/dist/mcp/wiki-server.d.ts +34 -23
  366. package/dist/mcp/wiki-server.d.ts.map +1 -1
  367. package/dist/mcp/wiki-server.js +9 -2
  368. package/dist/mcp/wiki-server.js.map +1 -1
  369. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  370. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  371. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  372. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  373. package/dist/modes/base.d.ts.map +1 -1
  374. package/dist/modes/base.js +5 -5
  375. package/dist/modes/base.js.map +1 -1
  376. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  377. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  378. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  379. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  380. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  381. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  382. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  383. package/dist/notifications/http-client.js.map +1 -1
  384. package/dist/notifications/index.d.ts +4 -1
  385. package/dist/notifications/index.d.ts.map +1 -1
  386. package/dist/notifications/index.js +3 -9
  387. package/dist/notifications/index.js.map +1 -1
  388. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  389. package/dist/notifications/reply-listener.d.ts.map +1 -1
  390. package/dist/notifications/reply-listener.js.map +1 -1
  391. package/dist/notifications/session-registry.js.map +1 -1
  392. package/dist/notifications/session-status.js.map +1 -1
  393. package/dist/notifications/temp-contract.d.ts.map +1 -1
  394. package/dist/notifications/temp-contract.js +4 -0
  395. package/dist/notifications/temp-contract.js.map +1 -1
  396. package/dist/notifications/tmux-detector.js.map +1 -1
  397. package/dist/notifications/tmux.js.map +1 -1
  398. package/dist/openclaw/config.js.map +1 -1
  399. package/dist/openclaw/dispatcher.js.map +1 -1
  400. package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
  401. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  402. package/dist/pipeline/__tests__/stages.test.js +16 -13
  403. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  404. package/dist/pipeline/stages/code-review.js.map +1 -1
  405. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  406. package/dist/pipeline/stages/ralplan.js.map +1 -1
  407. package/dist/pipeline/stages/team-exec.js.map +1 -1
  408. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  409. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  410. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  411. package/dist/planning/artifact-names.js.map +1 -1
  412. package/dist/planning/artifacts.js.map +1 -1
  413. package/dist/question/__tests__/state.test.js.map +1 -1
  414. package/dist/question/__tests__/ui.test.js.map +1 -1
  415. package/dist/question/autopilot-wait.js.map +1 -1
  416. package/dist/question/client.d.ts.map +1 -1
  417. package/dist/question/client.js.map +1 -1
  418. package/dist/question/deep-interview.js.map +1 -1
  419. package/dist/question/events.js.map +1 -1
  420. package/dist/question/renderer.js.map +1 -1
  421. package/dist/question/state.d.ts.map +1 -1
  422. package/dist/question/state.js.map +1 -1
  423. package/dist/question/ui.js.map +1 -1
  424. package/dist/ralph/contract.d.ts +1 -1
  425. package/dist/ralph/contract.d.ts.map +1 -1
  426. package/dist/ralplan/__tests__/consensus-gate.test.js +527 -8
  427. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  428. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  429. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  430. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
  431. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  432. package/dist/ralplan/__tests__/runtime.test.js +116 -14
  433. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  434. package/dist/ralplan/consensus-gate.d.ts +3 -3
  435. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  436. package/dist/ralplan/consensus-gate.js +173 -70
  437. package/dist/ralplan/consensus-gate.js.map +1 -1
  438. package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
  439. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  440. package/dist/ralplan/documented-leader-preflight.js +58 -0
  441. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  442. package/dist/ralplan/runtime.d.ts +3 -2
  443. package/dist/ralplan/runtime.d.ts.map +1 -1
  444. package/dist/ralplan/runtime.js +12 -9
  445. package/dist/ralplan/runtime.js.map +1 -1
  446. package/dist/runtime/__tests__/bridge.test.js +42 -1
  447. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  448. package/dist/runtime/bridge.d.ts +12 -0
  449. package/dist/runtime/bridge.d.ts.map +1 -1
  450. package/dist/runtime/bridge.js +52 -1
  451. package/dist/runtime/bridge.js.map +1 -1
  452. package/dist/runtime/process-tree.js.map +1 -1
  453. package/dist/runtime/run-loop.js.map +1 -1
  454. package/dist/runtime/run-outcome.d.ts +3 -3
  455. package/dist/runtime/run-outcome.d.ts.map +1 -1
  456. package/dist/runtime/run-outcome.js.map +1 -1
  457. package/dist/runtime/run-state.js.map +1 -1
  458. package/dist/scripts/__tests__/codex-native-hook.test.js +6657 -995
  459. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  460. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  461. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  462. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  463. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  464. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  465. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  466. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  467. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  470. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  471. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  472. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  473. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  474. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  475. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  476. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  477. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  478. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  479. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +78 -0
  480. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  481. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  482. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  483. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +55 -0
  484. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  485. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  486. package/dist/scripts/__tests__/smoke-packed-install.test.js +1100 -4
  487. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  488. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  489. package/dist/scripts/build-api.js.map +1 -1
  490. package/dist/scripts/build-explore-harness.js.map +1 -1
  491. package/dist/scripts/build-sparkshell.js.map +1 -1
  492. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  493. package/dist/scripts/codex-execution-surface.js.map +1 -1
  494. package/dist/scripts/codex-native-hook.d.ts +4 -1
  495. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  496. package/dist/scripts/codex-native-hook.js +11577 -1184
  497. package/dist/scripts/codex-native-hook.js.map +1 -1
  498. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  499. package/dist/scripts/generate-release-body.js.map +1 -1
  500. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  501. package/dist/scripts/hook-payload-guard.js.map +1 -1
  502. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  503. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  504. package/dist/scripts/notify-fallback-delivery.js +419 -0
  505. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  506. package/dist/scripts/notify-fallback-watcher.js +218 -138
  507. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  508. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  509. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  510. package/dist/scripts/notify-hook/auto-nudge.js +112 -15
  511. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  512. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  513. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  514. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  515. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  516. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  517. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  518. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  519. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  520. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  521. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  522. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  523. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/state-io.js +73 -11
  525. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  526. package/dist/scripts/notify-hook/team-dispatch.d.ts +10 -1
  527. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  528. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  529. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  530. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  531. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  532. package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
  533. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  534. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +16 -8
  535. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  537. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  538. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  539. package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +5 -2
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +37 -28
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +53 -18
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  552. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  553. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  554. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  555. package/dist/scripts/notify-hook.d.ts +24 -1
  556. package/dist/scripts/notify-hook.d.ts.map +1 -1
  557. package/dist/scripts/notify-hook.js +267 -90
  558. package/dist/scripts/notify-hook.js.map +1 -1
  559. package/dist/scripts/postinstall.js.map +1 -1
  560. package/dist/scripts/prompt-inventory.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +927 -5
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +4317 -13
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  566. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  567. package/dist/scripts/test-sparkshell.js.map +1 -1
  568. package/dist/scripts/verify-native-agents.js.map +1 -1
  569. package/dist/session-history/friction.js.map +1 -1
  570. package/dist/session-history/search.js.map +1 -1
  571. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  572. package/dist/sidecar/collector.js.map +1 -1
  573. package/dist/sidecar/index.js.map +1 -1
  574. package/dist/sidecar/render.js.map +1 -1
  575. package/dist/sidecar/tmux.js.map +1 -1
  576. package/dist/state/__tests__/mode-state-context.test.js +54 -0
  577. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  578. package/dist/state/__tests__/operations.test.js +66 -15
  579. package/dist/state/__tests__/operations.test.js.map +1 -1
  580. package/dist/state/__tests__/skill-active.test.js +19 -1
  581. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  582. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  583. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  584. package/dist/state/mode-state-context.d.ts +5 -0
  585. package/dist/state/mode-state-context.d.ts.map +1 -1
  586. package/dist/state/mode-state-context.js +59 -0
  587. package/dist/state/mode-state-context.js.map +1 -1
  588. package/dist/state/operations.d.ts +1 -1
  589. package/dist/state/operations.d.ts.map +1 -1
  590. package/dist/state/operations.js +49 -55
  591. package/dist/state/operations.js.map +1 -1
  592. package/dist/state/paths.d.ts +1 -1
  593. package/dist/state/paths.d.ts.map +1 -1
  594. package/dist/state/paths.js +1 -1
  595. package/dist/state/paths.js.map +1 -1
  596. package/dist/state/skill-active.d.ts +13 -1
  597. package/dist/state/skill-active.d.ts.map +1 -1
  598. package/dist/state/skill-active.js +43 -4
  599. package/dist/state/skill-active.js.map +1 -1
  600. package/dist/state/terminal-normalization.js.map +1 -1
  601. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  602. package/dist/state/workflow-transition-reconcile.js +20 -4
  603. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  604. package/dist/state/workflow-transition.d.ts +1 -1
  605. package/dist/state/workflow-transition.d.ts.map +1 -1
  606. package/dist/state/workflow-transition.js.map +1 -1
  607. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  608. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  609. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +52 -0
  610. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  611. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  612. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  613. package/dist/subagents/role-routing-marker.d.ts +10 -0
  614. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  615. package/dist/subagents/role-routing-marker.js +136 -0
  616. package/dist/subagents/role-routing-marker.js.map +1 -0
  617. package/dist/subagents/tracker.d.ts +91 -0
  618. package/dist/subagents/tracker.d.ts.map +1 -1
  619. package/dist/subagents/tracker.js +806 -62
  620. package/dist/subagents/tracker.js.map +1 -1
  621. package/dist/team/__tests__/api-interop.test.js +91 -1
  622. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  623. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  624. package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
  625. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  626. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  627. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  628. package/dist/team/__tests__/mcp-comm.test.js +93 -27
  629. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  630. package/dist/team/__tests__/model-contract.test.js +341 -4
  631. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  632. package/dist/team/__tests__/role-router.test.js.map +1 -1
  633. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  634. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  635. package/dist/team/__tests__/runtime.test.js +3624 -355
  636. package/dist/team/__tests__/runtime.test.js.map +1 -1
  637. package/dist/team/__tests__/scaling.test.js +2516 -118
  638. package/dist/team/__tests__/scaling.test.js.map +1 -1
  639. package/dist/team/__tests__/state-root.test.js.map +1 -1
  640. package/dist/team/__tests__/state.test.js +804 -36
  641. package/dist/team/__tests__/state.test.js.map +1 -1
  642. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  643. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  644. package/dist/team/__tests__/tmux-session.test.js +3664 -969
  645. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  646. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts +2 -2
  652. package/dist/team/api-interop.d.ts.map +1 -1
  653. package/dist/team/api-interop.js +15 -9
  654. package/dist/team/api-interop.js.map +1 -1
  655. package/dist/team/commit-hygiene.js.map +1 -1
  656. package/dist/team/contracts.d.ts +5 -5
  657. package/dist/team/contracts.d.ts.map +1 -1
  658. package/dist/team/delivery-log.js.map +1 -1
  659. package/dist/team/exact-pane.d.ts +23 -0
  660. package/dist/team/exact-pane.d.ts.map +1 -0
  661. package/dist/team/exact-pane.js +133 -0
  662. package/dist/team/exact-pane.js.map +1 -0
  663. package/dist/team/followup-planner.js.map +1 -1
  664. package/dist/team/goal-workflow.js.map +1 -1
  665. package/dist/team/idle-nudge.d.ts +12 -1
  666. package/dist/team/idle-nudge.d.ts.map +1 -1
  667. package/dist/team/idle-nudge.js +41 -14
  668. package/dist/team/idle-nudge.js.map +1 -1
  669. package/dist/team/model-contract.d.ts +18 -2
  670. package/dist/team/model-contract.d.ts.map +1 -1
  671. package/dist/team/model-contract.js +314 -35
  672. package/dist/team/model-contract.js.map +1 -1
  673. package/dist/team/orchestrator.js.map +1 -1
  674. package/dist/team/pane-status.js.map +1 -1
  675. package/dist/team/phase-controller.js.map +1 -1
  676. package/dist/team/reminder-intents.d.ts +1 -1
  677. package/dist/team/reminder-intents.d.ts.map +1 -1
  678. package/dist/team/reminder-intents.js.map +1 -1
  679. package/dist/team/runtime-cli.js.map +1 -1
  680. package/dist/team/runtime.d.ts +1 -0
  681. package/dist/team/runtime.d.ts.map +1 -1
  682. package/dist/team/runtime.js +1326 -231
  683. package/dist/team/runtime.js.map +1 -1
  684. package/dist/team/scaling.d.ts +22 -8
  685. package/dist/team/scaling.d.ts.map +1 -1
  686. package/dist/team/scaling.js +1463 -376
  687. package/dist/team/scaling.js.map +1 -1
  688. package/dist/team/state/dispatch-lock.js.map +1 -1
  689. package/dist/team/state/dispatch.d.ts.map +1 -1
  690. package/dist/team/state/dispatch.js +26 -8
  691. package/dist/team/state/dispatch.js.map +1 -1
  692. package/dist/team/state/events.js.map +1 -1
  693. package/dist/team/state/locks.d.ts.map +1 -1
  694. package/dist/team/state/locks.js +61 -13
  695. package/dist/team/state/locks.js.map +1 -1
  696. package/dist/team/state/tasks.d.ts.map +1 -1
  697. package/dist/team/state/tasks.js +4 -3
  698. package/dist/team/state/tasks.js.map +1 -1
  699. package/dist/team/state-root.js.map +1 -1
  700. package/dist/team/state.d.ts +67 -1
  701. package/dist/team/state.d.ts.map +1 -1
  702. package/dist/team/state.js +636 -129
  703. package/dist/team/state.js.map +1 -1
  704. package/dist/team/team-identity.d.ts.map +1 -1
  705. package/dist/team/team-identity.js.map +1 -1
  706. package/dist/team/team-ops.d.ts +5 -0
  707. package/dist/team/team-ops.d.ts.map +1 -1
  708. package/dist/team/team-ops.js +4 -0
  709. package/dist/team/team-ops.js.map +1 -1
  710. package/dist/team/tmux-session.d.ts +125 -23
  711. package/dist/team/tmux-session.d.ts.map +1 -1
  712. package/dist/team/tmux-session.js +1340 -333
  713. package/dist/team/tmux-session.js.map +1 -1
  714. package/dist/team/worker-bootstrap.js.map +1 -1
  715. package/dist/team/worktree.d.ts.map +1 -1
  716. package/dist/team/worktree.js +4 -0
  717. package/dist/team/worktree.js.map +1 -1
  718. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  721. package/dist/ultragoal/artifacts.js +4 -1
  722. package/dist/ultragoal/artifacts.js.map +1 -1
  723. package/dist/url-reader/index.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +57 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/agents-model-table.js.map +1 -1
  729. package/dist/utils/file-durability.d.ts +15 -0
  730. package/dist/utils/file-durability.d.ts.map +1 -0
  731. package/dist/utils/file-durability.js +37 -0
  732. package/dist/utils/file-durability.js.map +1 -0
  733. package/dist/utils/package.js.map +1 -1
  734. package/dist/utils/paths.js.map +1 -1
  735. package/dist/utils/platform-command.js.map +1 -1
  736. package/dist/utils/repo-deps.js.map +1 -1
  737. package/dist/utils/version.js.map +1 -1
  738. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/verifier.js.map +1 -1
  742. package/dist/visual/constants.d.ts +1 -1
  743. package/dist/visual/constants.d.ts.map +1 -1
  744. package/dist/vscode/__tests__/index.test.js.map +1 -1
  745. package/dist/vscode/index.js.map +1 -1
  746. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  747. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  748. package/dist/wiki/__tests__/query.test.js.map +1 -1
  749. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  750. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  751. package/dist/wiki/lint.d.ts +4 -1
  752. package/dist/wiki/lint.d.ts.map +1 -1
  753. package/dist/wiki/lint.js +3 -2
  754. package/dist/wiki/lint.js.map +1 -1
  755. package/dist/wiki/query.js.map +1 -1
  756. package/dist/wiki/storage.js.map +1 -1
  757. package/package.json +2 -2
  758. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  759. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
  760. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  761. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -2
  762. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +6 -2
  763. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  764. package/skills/omx-setup/SKILL.md +10 -5
  765. package/skills/ralph/SKILL.md +6 -2
  766. package/skills/ralplan/SKILL.md +6 -2
  767. package/skills/team/SKILL.md +13 -9
  768. package/src/scripts/__tests__/codex-native-hook.test.ts +20048 -10827
  769. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  770. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  772. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  773. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +78 -0
  774. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +60 -0
  775. package/src/scripts/__tests__/smoke-packed-install.test.ts +1309 -4
  776. package/src/scripts/codex-native-hook.ts +11429 -1163
  777. package/src/scripts/notify-fallback-delivery.ts +416 -0
  778. package/src/scripts/notify-fallback-watcher.ts +216 -155
  779. package/src/scripts/notify-hook/auto-nudge.ts +113 -14
  780. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  781. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  782. package/src/scripts/notify-hook/state-io.ts +100 -13
  783. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  784. package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
  785. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  786. package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
  787. package/src/scripts/notify-hook/team-worker.ts +39 -26
  788. package/src/scripts/notify-hook/tmux-injection.ts +66 -23
  789. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  790. package/src/scripts/notify-hook.ts +336 -90
  791. package/src/scripts/smoke-packed-install.ts +5154 -149
@@ -1,10 +1,14 @@
1
+ import { createHash } from 'crypto';
1
2
  import { join, resolve, dirname } from 'path';
2
- import { existsSync, appendFileSync, mkdirSync } from 'fs';
3
+ import { existsSync, appendFileSync, mkdirSync, readFileSync } from 'fs';
3
4
  import { mkdir, readdir, readFile, writeFile } from 'fs/promises';
4
5
  import { performance } from 'perf_hooks';
5
6
  import { spawn, spawnSync } from 'child_process';
6
- import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, buildWorkerProcessLaunchSpec, scrubTeamWorkerHudOwnershipEnv, resolveTeamWorkerCli, resolveTeamWorkerCliForResolvedLaunchArgs, resolveTeamWorkerLaunchMode, waitForWorkerReady, waitForWorkerReadyAsync, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafety, sendToWorker, sendToWorkerStdin, isWorkerAlive, isWorkerPaneOpen, getWorkerPanePid, killWorkerByPaneIdAsync, paneHasOmxInstanceTag, readPaneTeamOwnerTagResult, restoreStandaloneHudPane, teardownWorkerPanes, unregisterResizeHook, destroyTeamSession, listPaneIds, listTeamSessions, resolveSharedSessionShutdownTopology, } from './tmux-session.js';
7
- import { teamInit as initTeamState, DEFAULT_MAX_WORKERS, teamReadConfig as readTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadWorkerHeartbeat as readWorkerHeartbeat, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerInbox as writeWorkerInbox, teamCreateTask as createStateTask, teamReadTask as readTask, teamListTasks as listTasks, teamUpdateTask as updateTask, teamReadManifest as readTeamManifestV2, teamNormalizeGovernance as normalizeTeamGovernance, teamNormalizePolicy as normalizeTeamPolicy, teamClaimTask as claimTask, teamReleaseTaskClaim as releaseTaskClaim, teamReclaimExpiredTaskClaim as reclaimExpiredTaskClaim, teamAppendEvent as appendTeamEvent, teamReadTaskApproval as readTaskApproval, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamEnqueueDispatchRequest as enqueueDispatchRequest, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamTransitionDispatchRequest as transitionDispatchRequest, teamReadDispatchRequest as readDispatchRequest, teamCleanup as cleanupTeamState, teamSaveConfig as saveTeamConfig, teamWriteShutdownRequest as writeShutdownRequest, teamReadShutdownAck as readShutdownAck, teamReadMonitorSnapshot as readMonitorSnapshot, teamWriteMonitorSnapshot as writeMonitorSnapshot, teamReadPhase as readTeamPhaseState, teamWritePhase as writeTeamPhaseState, teamWriteWorkerStatus as writeWorkerStatus, writeAtomic, } from './team-ops.js';
7
+ import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, CreateTeamSessionPartialError, buildWorkerProcessLaunchSpec, scrubTeamWorkerHudOwnershipEnv, resolveTeamWorkerCli, resolveTeamWorkerCliForResolvedLaunchArgs, assertTeamWorkerCliPolicyCompatibility, resolveTeamWorkerLaunchMode, waitForWorkerReady, waitForWorkerReadyAsync, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafety, sendToWorker, sendToWorkerStdin, isWorkerAlive, isWorkerPaneOpen, paneHasOmxInstanceTag, readPaneTeamOwnerTagResult, restoreStandaloneHudPane, finalizeRestoredHudCleanupDebtSync, reconcileRestoredHudCleanupDebtSync, teardownWorkerPanes, unregisterResizeHook, queryDetachedTeamSession, requestDetachedTeamSessionDestroy, queryDetachedSessionLeaderBinding, listPaneIds, listTeamSessions, resolveSharedSessionShutdownTopology, } from './tmux-session.js';
8
+ import { readExactPaneProofSync, readExactPaneProofsSync } from './exact-pane.js';
9
+ import { reconcileScaleDownCleanupDebt } from './scaling.js';
10
+ import { teamInit as initTeamState, DEFAULT_MAX_WORKERS, teamReadConfig as readTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadWorkerHeartbeat as readWorkerHeartbeat, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerInbox as writeWorkerInbox, teamCreateTask as createStateTask, teamReadTask as readTask, teamListTasks as listTasks, teamUpdateTask as updateTask, teamReadManifest as readTeamManifestV2, teamNormalizeGovernance as normalizeTeamGovernance, teamNormalizePolicy as normalizeTeamPolicy, teamClaimTask as claimTask, teamReleaseTaskClaim as releaseTaskClaim, teamReclaimExpiredTaskClaim as reclaimExpiredTaskClaim, teamAppendEvent as appendTeamEvent, teamReadTaskApproval as readTaskApproval, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamEnqueueDispatchRequest as enqueueDispatchRequest, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamTransitionDispatchRequest as transitionDispatchRequest, teamReadDispatchRequest as readDispatchRequest, teamCleanup as cleanupTeamState, teamSaveConfig as saveTeamConfig, teamWriteShutdownRequest as writeShutdownRequest, teamReadShutdownAck as readShutdownAck, teamReadMonitorSnapshot as readMonitorSnapshot, teamWriteMonitorSnapshot as writeMonitorSnapshot, teamReadPhase as readTeamPhaseState, teamWritePhase as writeTeamPhaseState, teamWriteWorkerStatus as writeWorkerStatus, writeAtomic, teamWithTaskMembershipBarrier as withTeamTaskMembershipBarrier, teamRemoveDurableFile as removeDurableFile, } from './team-ops.js';
11
+ import { commitTeamMembershipTaskTransaction, writeTeamManifestV2 as writeTeamManifestV2State } from './state.js';
8
12
  import { queueInboxInstruction, queueDirectMailboxMessage, queueBroadcastMailboxMessage, waitForDispatchReceipt, } from './mcp-comm.js';
9
13
  import { appendTeamDeliveryLogForCwd } from './delivery-log.js';
10
14
  import { remapRepoAwareDecompositionMetadataToCreatedTasks, } from './repo-aware-decomposition.js';
@@ -16,7 +20,7 @@ import { loadRolePrompt } from './role-router.js';
16
20
  import { composeRoleInstructionsForRole } from '../agents/native-config.js';
17
21
  import { codexPromptsDir } from '../utils/paths.js';
18
22
  import { isTerminalPhase } from './orchestrator.js';
19
- import { resolveTeamWorkerLaunchArgs, resolveTeamWorkerLaunchDiagnostics, TEAM_LOW_COMPLEXITY_DEFAULT_MODEL, TEAM_WORKER_INHERITED_MODEL_ENV, parseTeamWorkerLaunchArgs, resolveAgentDefaultModel, resolveAgentReasoningEffort, shouldHonorAgentExactModel, } from './model-contract.js';
23
+ import { resolveTeamWorkerLaunchDiagnostics, TEAM_LOW_COMPLEXITY_DEFAULT_MODEL, TEAM_WORKER_INHERITED_MODEL_ENV, parseTeamWorkerLaunchArgs, resolveAgentDefaultModel, resolveAgentReasoningEffort, shouldHonorAgentExactModel, } from './model-contract.js';
20
24
  import { resolveCanonicalTeamStateRoot } from './state-root.js';
21
25
  import { buildInternalTeamName, resolveTeamIdentityScope, resolveTeamNameForCurrentContext } from './team-identity.js';
22
26
  import { inferPhaseTargetFromTaskCounts, reconcilePhaseStateForMonitor } from './phase-controller.js';
@@ -109,6 +113,40 @@ async function syncTeamModeStateOnShutdown(teamName, cwd, leaderSessionId) {
109
113
  await syncExactTeamModeStateOnShutdown(teamName, cwd, normalizedLeaderSessionId);
110
114
  }
111
115
  }
116
+ async function mutateShutdownConfig(config, cwd, mutate) {
117
+ return await withTeamTaskMembershipBarrier(config.name, cwd, async () => {
118
+ const current = await readTeamConfig(config.name, cwd);
119
+ if (!current)
120
+ throw new Error(`shutdown_config_missing:${config.name}`);
121
+ const baseGeneration = current.config_generation ?? 0;
122
+ const teamRoot = join(current.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', current.name);
123
+ const [currentConfigBytes, currentManifestBytes] = await Promise.all([
124
+ readFile(join(teamRoot, 'config.json'), 'utf8'),
125
+ existsSync(join(teamRoot, 'manifest.v2.json')) ? readFile(join(teamRoot, 'manifest.v2.json'), 'utf8') : null,
126
+ ]);
127
+ mutate(current);
128
+ const currentManifest = currentManifestBytes === null ? null : JSON.parse(currentManifestBytes);
129
+ await commitTeamMembershipTaskTransaction(current.name, cwd, {
130
+ baseGeneration,
131
+ tasks: [],
132
+ config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(current, null, 2) },
133
+ manifest: {
134
+ oldBytes: currentManifestBytes,
135
+ newBytes: currentManifest === null ? null : JSON.stringify({
136
+ ...currentManifest,
137
+ hud_pane_id: current.hud_pane_id,
138
+ hud_pane_pid: current.hud_pane_pid,
139
+ resize_hook_name: current.resize_hook_name,
140
+ resize_hook_target: current.resize_hook_target,
141
+ }, null, 2),
142
+ },
143
+ });
144
+ const committed = await readTeamConfig(current.name, cwd);
145
+ if (!committed)
146
+ throw new Error(`shutdown_config_missing_after_commit:${current.name}`);
147
+ return committed;
148
+ });
149
+ }
112
150
  async function assertTeamStartupIsNonDestructive(teamName, cwd, leaderSessionId) {
113
151
  const activeTeams = await findActiveTeams(cwd, leaderSessionId);
114
152
  if (activeTeams.length > 0) {
@@ -131,16 +169,44 @@ async function assertTeamStartupIsNonDestructive(teamName, cwd, leaderSessionId)
131
169
  }
132
170
  export function applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds) {
133
171
  config.tmux_session = createdSession.name;
172
+ config.tmux_session_id = createdSession.tmuxSessionId;
173
+ config.tmux_session_created = createdSession.tmuxSessionCreated;
134
174
  config.leader_pane_id = createdSession.leaderPaneId;
135
175
  config.hud_pane_id = createdSession.hudPaneId;
176
+ config.leader_pane_pid = typeof createdSession.leaderPanePid === 'number' && Number.isSafeInteger(createdSession.leaderPanePid) && createdSession.leaderPanePid > 0
177
+ ? createdSession.leaderPanePid
178
+ : null;
179
+ config.hud_pane_pid = typeof createdSession.hudPanePid === 'number' && Number.isSafeInteger(createdSession.hudPanePid) && createdSession.hudPanePid > 0
180
+ ? createdSession.hudPanePid
181
+ : null;
136
182
  config.tmux_pane_owner_id = createdSession.teamPaneOwnerId;
137
183
  config.resize_hook_name = createdSession.resizeHookName;
138
184
  config.resize_hook_target = createdSession.resizeHookTarget;
185
+ const paneIdsByIndex = createdSession.workerPaneIdsByIndex;
186
+ const panePidsByIndex = createdSession.workerPanePidsByIndex;
187
+ if (paneIdsByIndex) {
188
+ for (let i = 0; i < paneIdsByIndex.length; i++) {
189
+ const paneId = paneIdsByIndex[i];
190
+ if (!paneId)
191
+ continue;
192
+ workerPaneIds[i] = paneId;
193
+ if (config.workers[i]) {
194
+ config.workers[i].pane_id = paneId;
195
+ const panePid = panePidsByIndex?.[i];
196
+ if (typeof panePid === 'number' && Number.isSafeInteger(panePid) && panePid > 0)
197
+ config.workers[i].pid = panePid;
198
+ }
199
+ }
200
+ return;
201
+ }
139
202
  for (let i = 0; i < createdSession.workerPaneIds.length; i++) {
140
203
  const paneId = createdSession.workerPaneIds[i];
141
204
  workerPaneIds[i] = paneId;
142
205
  if (config.workers[i]) {
143
206
  config.workers[i].pane_id = paneId;
207
+ const panePid = createdSession.workerPanePidsByIndex?.[i];
208
+ if (typeof panePid === 'number' && Number.isSafeInteger(panePid) && panePid > 0)
209
+ config.workers[i].pid = panePid;
144
210
  }
145
211
  }
146
212
  }
@@ -167,25 +233,41 @@ function collectShutdownPaneIds(params) {
167
233
  }
168
234
  return [...paneIds];
169
235
  }
170
- function filterSharedSessionShutdownWorkerPaneIdsByOwner(paneIds, teamPaneOwnerId, legacyPersistedWorkerPaneIds = new Set(), onOwnerReadError) {
236
+ function collectAuthorizedSharedSessionWorkerPaneIds(paneIds, teamPaneOwnerId, canonicalWorkerPaneIds, initiallyTaggedWorkerPaneIds, authorizedTaggedWorkerPaneIds) {
171
237
  const expectedOwnerId = teamPaneOwnerId.trim();
172
- if (!expectedOwnerId)
173
- return [];
174
- return paneIds.filter((paneId) => {
175
- const actualOwnerId = readPaneTeamOwnerTagResult(paneId);
176
- if (actualOwnerId.status === 'value')
177
- return actualOwnerId.value === expectedOwnerId;
178
- if (actualOwnerId.status === 'missing') {
179
- // Legacy, already-running Team panes may not have @omx_team_pane_owner_id.
180
- // Keep that compatibility path explicitly bounded to panes that are both
181
- // live worker-command candidates and persisted in this team's state. This
182
- // preserves old Team cleanup without letting arbitrary worker-looking
183
- // panes become kill candidates merely because the owner tag is absent.
184
- return legacyPersistedWorkerPaneIds.has(paneId);
185
- }
186
- onOwnerReadError?.(paneId, actualOwnerId.error);
187
- return false;
188
- });
238
+ if (!expectedOwnerId && paneIds.length > 0) {
239
+ throw new Error('shutdown_shared_session_worker_owner_unavailable:missing_team_owner_id');
240
+ }
241
+ const authorized = new Set();
242
+ for (const paneId of paneIds) {
243
+ const owner = readPaneTeamOwnerTagResult(paneId);
244
+ if (initiallyTaggedWorkerPaneIds?.has(paneId)
245
+ && (owner.status !== 'value' || owner.value !== expectedOwnerId)) {
246
+ throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
247
+ }
248
+ if (owner.status === 'error') {
249
+ // A read failure is authority ambiguity only for an explicit persisted
250
+ // worker that topology independently identifies as a worker candidate.
251
+ if (canonicalWorkerPaneIds.has(paneId)) {
252
+ throw new Error(`shutdown_shared_session_worker_owner_unavailable:${paneId}:${owner.error}`);
253
+ }
254
+ continue;
255
+ }
256
+ if (owner.status === 'value') {
257
+ if (owner.value === expectedOwnerId) {
258
+ authorized.add(paneId);
259
+ authorizedTaggedWorkerPaneIds?.add(paneId);
260
+ }
261
+ else if (canonicalWorkerPaneIds.has(paneId)) {
262
+ throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
263
+ }
264
+ continue;
265
+ }
266
+ if (canonicalWorkerPaneIds.has(paneId)) {
267
+ throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
268
+ }
269
+ }
270
+ return [...authorized];
189
271
  }
190
272
  function isSharedSessionHudPaneReclaimable(params) {
191
273
  const { paneId, persistedHudPaneId, leaderOwnedHudPaneIds, teamPaneOwnerId, onOwnerReadError } = params;
@@ -227,6 +309,209 @@ export function shouldPrekillInteractiveShutdownProcessTrees(sessionName) {
227
309
  // teardown alone is insufficient.
228
310
  return true;
229
311
  }
312
+ function canonicalDetachedStateRoot(config, cwd) {
313
+ return resolve(config?.team_state_root ?? resolveCanonicalTeamStateRoot(cwd));
314
+ }
315
+ function detachedSessionDestroyReceiptPath(teamName, cwd, config) {
316
+ return join(canonicalDetachedStateRoot(config ?? null, cwd), 'team', teamName, '.detached-session-destroy-receipt.json');
317
+ }
318
+ function detachedConfigIdentity(teamName, cwd, config) {
319
+ const ownerId = typeof config.tmux_pane_owner_id === 'string' ? config.tmux_pane_owner_id.trim() : '';
320
+ const paneId = typeof config.leader_pane_id === 'string' ? config.leader_pane_id.trim() : '';
321
+ const panePid = config.leader_pane_pid;
322
+ if (config.name !== teamName || !config.created_at || !config.tmux_session || !ownerId || !/^%[0-9]+$/.test(paneId)
323
+ || typeof panePid !== 'number' || !Number.isSafeInteger(panePid) || panePid <= 0)
324
+ return null;
325
+ // Bind the durable bytes of both canonical files, not only authorization
326
+ // fields. A worker/membership mutation invalidates a published receipt.
327
+ try {
328
+ const teamRoot = join(canonicalDetachedStateRoot(config, cwd), 'team', teamName);
329
+ const configBytes = readFileSync(join(teamRoot, 'config.json'), 'utf8');
330
+ const manifestBytes = readFileSync(join(teamRoot, 'manifest.v2.json'), 'utf8');
331
+ const parsedConfig = JSON.parse(configBytes);
332
+ const parsedManifest = JSON.parse(manifestBytes);
333
+ if (!Number.isSafeInteger(parsedConfig.config_generation)
334
+ || !Number.isSafeInteger(parsedManifest.config_generation)
335
+ || parsedConfig.config_generation !== parsedManifest.config_generation)
336
+ return null;
337
+ return createHash('sha256').update(JSON.stringify({
338
+ version: 2,
339
+ team_name: teamName,
340
+ state_root: canonicalDetachedStateRoot(config, cwd),
341
+ config_bytes: configBytes,
342
+ manifest_bytes: manifestBytes,
343
+ })).digest('hex');
344
+ }
345
+ catch {
346
+ return null;
347
+ }
348
+ }
349
+ /** Deterministic test seam for mutations after durable intent publication. */
350
+ let detachedSessionDestroyAfterJournalHook = null;
351
+ export function setDetachedSessionDestroyAfterJournalHookForTest(hook) {
352
+ detachedSessionDestroyAfterJournalHook = hook;
353
+ }
354
+ function isDetachedSessionDestroyReceipt(value) {
355
+ if (!value || typeof value !== 'object')
356
+ return false;
357
+ const receipt = value;
358
+ return receipt.schema_version === 2 && receipt.schema === 'omx.detached_session_destroy.v2'
359
+ && receipt.operation === 'detached_session_destroy' && (receipt.status === 'intent' || receipt.status === 'accepted')
360
+ && typeof receipt.team_name === 'string' && receipt.team_name.length > 0
361
+ && typeof receipt.state_root === 'string' && receipt.state_root.length > 0 && resolve(receipt.state_root) === receipt.state_root
362
+ && receipt.config_identity_version === 2 && typeof receipt.config_identity_digest === 'string' && /^[a-f0-9]{64}$/.test(receipt.config_identity_digest)
363
+ && typeof receipt.session_name === 'string' && receipt.session_name.length > 0 && !receipt.session_name.includes(':')
364
+ && typeof receipt.session_id === 'string' && receipt.session_id.length > 0 && !/\s/.test(receipt.session_id)
365
+ && typeof receipt.session_created === 'string' && /^[0-9]+$/.test(receipt.session_created)
366
+ && typeof receipt.leader_pane_id === 'string' && /^%[0-9]+$/.test(receipt.leader_pane_id)
367
+ && typeof receipt.leader_pane_pid === 'number' && Number.isSafeInteger(receipt.leader_pane_pid) && receipt.leader_pane_pid > 0
368
+ && typeof receipt.owner_id === 'string' && receipt.owner_id.length > 0;
369
+ }
370
+ async function readDetachedSessionDestroyReceipt(teamName, cwd, config) {
371
+ const path = detachedSessionDestroyReceiptPath(teamName, cwd, config);
372
+ if (!existsSync(path))
373
+ return null;
374
+ try {
375
+ const parsed = JSON.parse(await readFile(path, 'utf-8'));
376
+ if (!isDetachedSessionDestroyReceipt(parsed) || parsed.team_name !== teamName || parsed.state_root !== canonicalDetachedStateRoot(config, cwd))
377
+ throw new Error('invalid receipt');
378
+ return parsed;
379
+ }
380
+ catch {
381
+ throw new Error(`detached_session_destroy_receipt_malformed:${config?.tmux_session || 'missing_session'}`);
382
+ }
383
+ }
384
+ async function writeDetachedSessionDestroyReceipt(teamName, cwd, config, receipt) {
385
+ await writeAtomic(detachedSessionDestroyReceiptPath(teamName, cwd, config), `${JSON.stringify(receipt, null, 2)}\n`);
386
+ }
387
+ function validateDetachedReceiptConfig(teamName, cwd, config, receipt) {
388
+ if (receipt.team_name !== teamName || receipt.state_root !== canonicalDetachedStateRoot(config, cwd)
389
+ || receipt.config_identity_version !== 2 || detachedConfigIdentity(teamName, cwd, config) !== receipt.config_identity_digest
390
+ || config.tmux_session !== receipt.session_name || config.tmux_session_id !== receipt.session_id
391
+ || config.tmux_session_created !== receipt.session_created || config.leader_pane_id !== receipt.leader_pane_id
392
+ || config.leader_pane_pid !== receipt.leader_pane_pid || config.tmux_pane_owner_id !== receipt.owner_id) {
393
+ throw new Error(`detached_session_destroy_receipt_config_mismatch:${receipt.session_name}`);
394
+ }
395
+ }
396
+ function assertDetachedSessionDestroyAuthority(receipt) {
397
+ const expected = { sessionId: receipt.session_id, sessionCreated: receipt.session_created };
398
+ const query = queryDetachedTeamSession(receipt.session_name, expected);
399
+ if (query.status !== 'exact'
400
+ || !queryDetachedSessionLeaderBinding(receipt.leader_pane_id, receipt.leader_pane_pid, receipt.session_name, expected)) {
401
+ throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
402
+ }
403
+ const proof = readExactPaneProofSync(receipt.leader_pane_id);
404
+ if (proof.status !== 'live' || proof.pid !== receipt.leader_pane_pid)
405
+ throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
406
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
407
+ if (owner.status !== 'value' || owner.value !== receipt.owner_id)
408
+ throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
409
+ }
410
+ function requestDetachedSessionDestroyAtFinalSink(receipt) {
411
+ return requestDetachedTeamSessionDestroy(receipt.session_name, {
412
+ sessionId: receipt.session_id,
413
+ sessionCreated: receipt.session_created,
414
+ leaderPaneId: receipt.leader_pane_id,
415
+ leaderPanePid: receipt.leader_pane_pid,
416
+ ownerId: receipt.owner_id,
417
+ });
418
+ }
419
+ async function withDetachedSessionDestroyLock(teamName, cwd, fn) {
420
+ return await withTeamTaskMembershipBarrier(teamName, cwd, fn);
421
+ }
422
+ async function convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, config, receipt) {
423
+ const query = queryDetachedTeamSession(receipt.session_name, { sessionId: receipt.session_id, sessionCreated: receipt.session_created });
424
+ if (query.status === 'unavailable' || query.status === 'replacement')
425
+ throw new Error(`detached_session_destroy_unresolved:${receipt.session_name}`);
426
+ if (query.status === 'absent') {
427
+ if (config) {
428
+ validateDetachedReceiptConfig(teamName, cwd, config, receipt);
429
+ config.tmux_session = '';
430
+ await saveTeamConfig(config, cwd);
431
+ }
432
+ else if (receipt.status !== 'accepted') {
433
+ throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
434
+ }
435
+ return true;
436
+ }
437
+ if (!config)
438
+ throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
439
+ validateDetachedReceiptConfig(teamName, cwd, config, receipt);
440
+ assertDetachedSessionDestroyAuthority(receipt);
441
+ if (!requestDetachedSessionDestroyAtFinalSink(receipt)) {
442
+ throw new Error(`detached_session_destroy_unresolved:${receipt.session_name}`);
443
+ }
444
+ await writeDetachedSessionDestroyReceipt(teamName, cwd, config, { ...receipt, status: 'accepted' });
445
+ return await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, config, { ...receipt, status: 'accepted' });
446
+ }
447
+ async function reconcileDetachedSessionDestroyReceipt(teamName, cwd, config) {
448
+ return await withDetachedSessionDestroyLock(teamName, cwd, async () => {
449
+ const canonical = await readTeamConfig(teamName, cwd);
450
+ const receipt = await readDetachedSessionDestroyReceipt(teamName, cwd, canonical ?? config);
451
+ if (!receipt)
452
+ return false;
453
+ if (!canonical) {
454
+ // Name/file presence is not authority. Accepted absence requires a
455
+ // successfully parsed current canonical generation.
456
+ throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
457
+ }
458
+ return await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, canonical, receipt);
459
+ });
460
+ }
461
+ async function destroyConfiguredDetachedTeamSession(teamName, cwd, config) {
462
+ if (!config.tmux_session)
463
+ return;
464
+ await withDetachedSessionDestroyLock(teamName, cwd, async () => {
465
+ const canonical = await readTeamConfig(teamName, cwd);
466
+ if (!canonical)
467
+ throw new Error(`detached_session_destroy_authorization_unavailable:${config.tmux_session}`);
468
+ const existing = await readDetachedSessionDestroyReceipt(teamName, cwd, canonical);
469
+ if (existing) {
470
+ await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, canonical, existing);
471
+ return;
472
+ }
473
+ const sessionName = canonical.tmux_session;
474
+ const sessionId = typeof canonical.tmux_session_id === 'string' ? canonical.tmux_session_id : '';
475
+ const sessionCreated = typeof canonical.tmux_session_created === 'string' ? canonical.tmux_session_created : '';
476
+ const digest = detachedConfigIdentity(teamName, cwd, canonical);
477
+ const ownerId = typeof canonical.tmux_pane_owner_id === 'string' ? canonical.tmux_pane_owner_id.trim() : '';
478
+ const leaderPaneId = typeof canonical.leader_pane_id === 'string' ? canonical.leader_pane_id.trim() : '';
479
+ const leaderPanePid = canonical.leader_pane_pid;
480
+ if (!digest || !sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated) || !ownerId || !/^%[0-9]+$/.test(leaderPaneId)
481
+ || typeof leaderPanePid !== 'number' || !Number.isSafeInteger(leaderPanePid) || leaderPanePid <= 0) {
482
+ throw new Error(`detached_session_destroy_authorization_unavailable:${sessionName || 'missing_session'}`);
483
+ }
484
+ const receipt = {
485
+ schema_version: 2, schema: 'omx.detached_session_destroy.v2', operation: 'detached_session_destroy', status: 'intent',
486
+ team_name: teamName, state_root: canonicalDetachedStateRoot(canonical, cwd), config_identity_version: 2, config_identity_digest: digest,
487
+ session_name: sessionName, session_id: sessionId, session_created: sessionCreated,
488
+ leader_pane_id: leaderPaneId, leader_pane_pid: leaderPanePid, owner_id: ownerId,
489
+ };
490
+ assertDetachedSessionDestroyAuthority(receipt);
491
+ await writeDetachedSessionDestroyReceipt(teamName, cwd, canonical, receipt);
492
+ const afterJournalHook = detachedSessionDestroyAfterJournalHook;
493
+ detachedSessionDestroyAfterJournalHook = null;
494
+ await afterJournalHook?.();
495
+ const current = await readTeamConfig(teamName, cwd);
496
+ if (!current)
497
+ throw new Error(`detached_session_destroy_authorization_unavailable:${sessionName}`);
498
+ validateDetachedReceiptConfig(teamName, cwd, current, receipt);
499
+ assertDetachedSessionDestroyAuthority(receipt);
500
+ if (!requestDetachedSessionDestroyAtFinalSink(receipt)) {
501
+ throw new Error(`detached_session_destroy_unresolved:${sessionName}`);
502
+ }
503
+ await writeDetachedSessionDestroyReceipt(teamName, cwd, current, { ...receipt, status: 'accepted' });
504
+ await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, current, { ...receipt, status: 'accepted' });
505
+ });
506
+ }
507
+ function assertPaneTeardownProofsAvailable(operation, unavailableProofs) {
508
+ if (unavailableProofs.length === 0)
509
+ return;
510
+ const detail = unavailableProofs
511
+ .map((proof) => `${proof.paneId}:${proof.reason}${proof.detail ? `:${proof.detail}` : ''}`)
512
+ .join(',');
513
+ throw new Error(`${operation}_pane_proof_unavailable:${detail}`);
514
+ }
230
515
  export async function cleanupTeamWorkerLaunchOrphanedMcpProcesses(dependencies = {}) {
231
516
  const cleanup = dependencies.cleanup ?? (() => cleanupOmxMcpProcesses([], {
232
517
  selectCandidates: findLaunchSafeCleanupCandidates,
@@ -1132,9 +1417,9 @@ const PROMPT_MODE_CODEX_TEST_ALLOW_ENV = 'OMX_TEST_ALLOW_NONTTY_CODEX_PROMPT';
1132
1417
  function resolveInstructionStateRoot(worktreePath) {
1133
1418
  return worktreePath ? WORKTREE_TRIGGER_STATE_ROOT : undefined;
1134
1419
  }
1135
- function assertPromptModeWorkerCliSupported(workerCliPlan) {
1420
+ function assertPromptModeWorkerCliSupported(workerCliPlan, env = process.env) {
1136
1421
  if (workerCliPlan.some((workerCli) => workerCli === 'codex')
1137
- && process.env[PROMPT_MODE_CODEX_TEST_ALLOW_ENV] !== '1') {
1422
+ && env[PROMPT_MODE_CODEX_TEST_ALLOW_ENV] !== '1') {
1138
1423
  throw new Error(`${PROMPT_MODE_CODEX_UNSUPPORTED_REASON}: Codex prompt workers require a terminal; use interactive team mode or set OMX_TEAM_WORKER_CLI=claude/gemini for prompt-mode teammates.`);
1139
1424
  }
1140
1425
  }
@@ -1384,7 +1669,7 @@ function registerPromptWorkerHandle(teamName, workerName, child) {
1384
1669
  if (!teamHandles)
1385
1670
  return;
1386
1671
  const handle = teamHandles.get(workerName);
1387
- if (handle?.processGroupId && isProcessGroupAlive(handle.processGroupId)) {
1672
+ if (handle?.processGroupId && probeProcessGroupLiveness(handle.processGroupId) !== 'gone') {
1388
1673
  return;
1389
1674
  }
1390
1675
  teamHandles.delete(workerName);
@@ -1403,34 +1688,70 @@ function removePromptWorkerHandle(teamName, workerName) {
1403
1688
  if (teamHandles.size === 0)
1404
1689
  promptWorkerRegistry.delete(teamName);
1405
1690
  }
1406
- function isPidAlive(pid) {
1691
+ function probePidLiveness(pid) {
1407
1692
  if (!Number.isFinite(pid) || pid <= 0)
1408
- return false;
1693
+ return 'gone';
1409
1694
  try {
1410
1695
  process.kill(pid, 0);
1411
- return true;
1696
+ return 'alive';
1412
1697
  }
1413
1698
  catch (err) {
1414
1699
  if (err.code === 'ESRCH')
1415
- return false;
1700
+ return 'gone';
1416
1701
  process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
1417
- return false;
1702
+ return 'unknown';
1703
+ }
1704
+ }
1705
+ function isPidAlive(pid) {
1706
+ return probePidLiveness(pid) === 'alive';
1707
+ }
1708
+ function isPidGone(pid) {
1709
+ return probePidLiveness(pid) === 'gone';
1710
+ }
1711
+ async function captureProcessIdentity(pid) {
1712
+ // There is no portable process birth identifier exposed by Node. Refuse to
1713
+ // persist replayable PID debt on platforms without Linux's stable proc stat
1714
+ // start-time field rather than later treating a reused PID as authoritative.
1715
+ if (process.platform !== 'linux' || !Number.isSafeInteger(pid) || pid <= 0)
1716
+ return null;
1717
+ try {
1718
+ const stat = await readFile(`/proc/${pid}/stat`, 'utf8');
1719
+ const close = stat.lastIndexOf(')');
1720
+ const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/) : [];
1721
+ // field 22 is starttime; fields begin at field 3 after the comm value.
1722
+ const startTime = fields[19];
1723
+ return typeof startTime === 'string' && /^[0-9]+$/.test(startTime)
1724
+ ? { pid, start_time: startTime }
1725
+ : null;
1726
+ }
1727
+ catch {
1728
+ return null;
1418
1729
  }
1419
1730
  }
1420
- function isProcessGroupAlive(processGroupId) {
1731
+ async function captureProcessIdentities(pids) {
1732
+ const identities = await Promise.all([...new Set(pids)].map((pid) => captureProcessIdentity(pid)));
1733
+ return identities.every((identity) => identity !== null) ? identities : null;
1734
+ }
1735
+ async function probeProcessIdentity(identity) {
1736
+ const current = await captureProcessIdentity(identity.pid);
1737
+ if (current === null)
1738
+ return probePidLiveness(identity.pid) === 'gone' ? 'gone' : 'reused_or_unknown';
1739
+ return current.start_time === identity.start_time ? 'same' : 'reused_or_unknown';
1740
+ }
1741
+ function probeProcessGroupLiveness(processGroupId) {
1421
1742
  if (process.platform === 'win32')
1422
- return false;
1743
+ return 'gone';
1423
1744
  if (!Number.isFinite(processGroupId) || processGroupId <= 0)
1424
- return false;
1745
+ return 'gone';
1425
1746
  try {
1426
1747
  process.kill(-processGroupId, 0);
1427
- return true;
1748
+ return 'alive';
1428
1749
  }
1429
1750
  catch (err) {
1430
1751
  if (err.code === 'ESRCH')
1431
- return false;
1752
+ return 'gone';
1432
1753
  process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
1433
- return false;
1754
+ return 'unknown';
1434
1755
  }
1435
1756
  }
1436
1757
  function listProcessTreeEntries() {
@@ -1491,11 +1812,11 @@ async function waitForTrackedPidsExit(pids, timeoutMs) {
1491
1812
  return true;
1492
1813
  const deadline = Date.now() + Math.max(0, timeoutMs);
1493
1814
  while (Date.now() < deadline) {
1494
- if (tracked.every((pid) => !isPidAlive(pid)))
1815
+ if (tracked.every(isPidGone))
1495
1816
  return true;
1496
1817
  await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
1497
1818
  }
1498
- return tracked.every((pid) => !isPidAlive(pid));
1819
+ return tracked.every(isPidGone);
1499
1820
  }
1500
1821
  async function terminateTrackedProcessTree(rootPid, processGroupId = null, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS) {
1501
1822
  if (processGroupId && process.platform !== 'win32') {
@@ -1522,9 +1843,9 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
1522
1843
  }
1523
1844
  const groupDeadline = Date.now() + Math.max(0, graceMs);
1524
1845
  while (Date.now() < groupDeadline) {
1525
- const groupAlive = isProcessGroupAlive(processGroupId);
1526
- const descendantsAlive = trackedPids.some((pid) => isPidAlive(pid));
1527
- if (!groupAlive && !descendantsAlive) {
1846
+ const groupGone = probeProcessGroupLiveness(processGroupId) === 'gone';
1847
+ const descendantsGone = trackedPids.every(isPidGone);
1848
+ if (groupGone && descendantsGone) {
1528
1849
  return { terminated: true, forcedKill: false, trackedPids };
1529
1850
  }
1530
1851
  await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
@@ -1551,15 +1872,15 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
1551
1872
  }
1552
1873
  const killDeadline = Date.now() + Math.max(0, killWaitMs);
1553
1874
  while (Date.now() < killDeadline) {
1554
- const groupAlive = isProcessGroupAlive(processGroupId);
1555
- const descendantsAlive = trackedPids.some((pid) => isPidAlive(pid));
1556
- if (!groupAlive && !descendantsAlive) {
1875
+ const groupGone = probeProcessGroupLiveness(processGroupId) === 'gone';
1876
+ const descendantsGone = trackedPids.every(isPidGone);
1877
+ if (groupGone && descendantsGone) {
1557
1878
  return { terminated: true, forcedKill: true, trackedPids };
1558
1879
  }
1559
1880
  await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
1560
1881
  }
1561
1882
  return {
1562
- terminated: !isProcessGroupAlive(processGroupId) && trackedPids.every((pid) => !isPidAlive(pid)),
1883
+ terminated: probeProcessGroupLiveness(processGroupId) === 'gone' && trackedPids.every(isPidGone),
1563
1884
  forcedKill: true,
1564
1885
  trackedPids,
1565
1886
  };
@@ -1567,7 +1888,7 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
1567
1888
  const trackedPids = collectProcessTreePids(rootPid);
1568
1889
  if (trackedPids.length === 0) {
1569
1890
  return {
1570
- terminated: !isPidAlive(rootPid),
1891
+ terminated: isPidGone(rootPid),
1571
1892
  forcedKill: false,
1572
1893
  trackedPids: [],
1573
1894
  };
@@ -1603,6 +1924,327 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
1603
1924
  trackedPids,
1604
1925
  };
1605
1926
  }
1927
+ /**
1928
+ * Every operating-system effect attributed to an explicit pane must re-read
1929
+ * the pane's global row immediately before that effect. A changed/dead row is
1930
+ * not a fallback opportunity: it stops this pane's process-tree teardown.
1931
+ */
1932
+ function reproveExactPaneAfterProcessEsrcH(paneId) {
1933
+ const proof = readExactPaneProofSync(paneId);
1934
+ if (proof.status === 'unavailable')
1935
+ return { status: 'unavailable', proof };
1936
+ if (proof.status === 'gone')
1937
+ return { status: 'gone' };
1938
+ // ESRCH establishes only that the process was absent. A still-live pane row
1939
+ // is not authority to advance the pane lifecycle without an absent reproof.
1940
+ return { status: 'stopped' };
1941
+ }
1942
+ function signalExactPaneProcess(paneId, authorizedPanePid, pid, signal) {
1943
+ const proof = readExactPaneProofSync(paneId);
1944
+ if (proof.status === 'unavailable')
1945
+ return { status: 'unavailable', proof };
1946
+ if (proof.status === 'live' && proof.pid !== authorizedPanePid)
1947
+ return { status: 'stopped' };
1948
+ if (proof.status === 'gone')
1949
+ return { status: 'stopped' };
1950
+ try {
1951
+ process.kill(pid, signal);
1952
+ return { status: 'alive' };
1953
+ }
1954
+ catch (error) {
1955
+ return error.code === 'ESRCH'
1956
+ ? reproveExactPaneAfterProcessEsrcH(paneId)
1957
+ : { status: 'unknown' };
1958
+ }
1959
+ }
1960
+ function probeExactPaneProcess(paneId, authorizedPanePid, pid) {
1961
+ const proof = readExactPaneProofSync(paneId);
1962
+ if (proof.status === 'unavailable')
1963
+ return { status: 'unavailable', proof };
1964
+ if (proof.status === 'live' && proof.pid !== authorizedPanePid)
1965
+ return { status: 'stopped' };
1966
+ try {
1967
+ process.kill(pid, 0);
1968
+ return proof.status === 'gone' ? { status: 'stopped' } : { status: 'alive' };
1969
+ }
1970
+ catch (error) {
1971
+ return error.code === 'ESRCH'
1972
+ ? reproveExactPaneAfterProcessEsrcH(paneId)
1973
+ : { status: 'unknown' };
1974
+ }
1975
+ }
1976
+ async function waitForExactPaneTrackedPidsExit(paneId, authorizedPanePid, pids, timeoutMs) {
1977
+ const trackedPids = [...pids];
1978
+ const deadline = Date.now() + Math.max(0, timeoutMs);
1979
+ do {
1980
+ let anyAlive = false;
1981
+ for (const pid of trackedPids) {
1982
+ const probe = probeExactPaneProcess(paneId, authorizedPanePid, pid);
1983
+ if (probe.status === 'unavailable')
1984
+ return { terminated: false, stopped: true, trackedPids, authorizedPanePid, proofUnavailable: probe.proof };
1985
+ if (probe.status === 'stopped')
1986
+ return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
1987
+ if (probe.status === 'unknown')
1988
+ return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
1989
+ if (probe.status === 'alive')
1990
+ anyAlive = true;
1991
+ }
1992
+ if (!anyAlive)
1993
+ return { terminated: true, stopped: false, trackedPids, authorizedPanePid };
1994
+ if (Date.now() >= deadline)
1995
+ break;
1996
+ await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
1997
+ } while (Date.now() < deadline);
1998
+ for (const pid of trackedPids) {
1999
+ const probe = probeExactPaneProcess(paneId, authorizedPanePid, pid);
2000
+ if (probe.status === 'unavailable')
2001
+ return { terminated: false, stopped: true, trackedPids, authorizedPanePid, proofUnavailable: probe.proof };
2002
+ if (probe.status === 'stopped' || probe.status === 'unknown')
2003
+ return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
2004
+ if (probe.status === 'alive')
2005
+ return { terminated: false, stopped: false, trackedPids, authorizedPanePid };
2006
+ }
2007
+ return { terminated: true, stopped: false, trackedPids, authorizedPanePid };
2008
+ }
2009
+ async function terminateExactPaneProcessTree(paneId, expectedPanePid, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS, authorizePaneEffect) {
2010
+ const authorization = readExactPaneProofSync(paneId);
2011
+ if (authorization.status === 'unavailable')
2012
+ return { terminated: false, stopped: true, trackedPids: [], proofUnavailable: authorization };
2013
+ if (authorization.status === 'gone')
2014
+ return { terminated: true, stopped: true, trackedPids: [] };
2015
+ if (typeof expectedPanePid === 'number' && authorization.pid !== expectedPanePid) {
2016
+ return { terminated: false, stopped: true, trackedPids: [], authorizedPanePid: authorization.pid, proofUnavailable: {
2017
+ status: 'unavailable', paneId, reason: 'pane_pid_changed', detail: `expected ${expectedPanePid}, got ${authorization.pid}`,
2018
+ } };
2019
+ }
2020
+ if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
2021
+ return {
2022
+ terminated: false,
2023
+ stopped: true,
2024
+ trackedPids: [],
2025
+ authorizedPanePid: authorization.pid,
2026
+ proofUnavailable: {
2027
+ status: 'unavailable',
2028
+ paneId,
2029
+ reason: 'pane_pid_changed',
2030
+ detail: 'pane owner authorization changed',
2031
+ },
2032
+ };
2033
+ }
2034
+ const trackedPids = collectProcessTreePids(authorization.pid);
2035
+ if (trackedPids.length === 0)
2036
+ return { terminated: true, stopped: false, trackedPids, authorizedPanePid: authorization.pid };
2037
+ // Birth evidence is mandatory for descendant signals. A pane's pinned PID
2038
+ // authorizes its root process, but a numeric descendant PID is never enough:
2039
+ // its /proc start time must still match immediately before each signal.
2040
+ const trackedProcessIdentities = await captureProcessIdentities(trackedPids) ?? undefined;
2041
+ const trackedProcessIdentityByPid = new Map((trackedProcessIdentities ?? []).map((identity) => [identity.pid, identity]));
2042
+ const revalidateDescendantIdentity = async (pid) => {
2043
+ if (pid === authorization.pid)
2044
+ return true;
2045
+ const identity = trackedProcessIdentityByPid.get(pid);
2046
+ return identity !== undefined && (await probeProcessIdentity(identity)) === 'same';
2047
+ };
2048
+ for (const pid of trackedPids) {
2049
+ if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
2050
+ return {
2051
+ terminated: false,
2052
+ stopped: true,
2053
+ trackedPids,
2054
+ trackedProcessIdentities,
2055
+ authorizedPanePid: authorization.pid,
2056
+ proofUnavailable: {
2057
+ status: 'unavailable',
2058
+ paneId,
2059
+ reason: 'pane_pid_changed',
2060
+ detail: 'pane owner authorization changed',
2061
+ },
2062
+ };
2063
+ }
2064
+ if (!await revalidateDescendantIdentity(pid)) {
2065
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
2066
+ }
2067
+ const signal = signalExactPaneProcess(paneId, authorization.pid, pid, 'SIGTERM');
2068
+ if (signal.status === 'unavailable')
2069
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: signal.proof };
2070
+ if (signal.status === 'stopped' || signal.status === 'unknown')
2071
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
2072
+ }
2073
+ const graceful = await waitForExactPaneTrackedPidsExit(paneId, authorization.pid, trackedPids, graceMs);
2074
+ if (graceful.terminated || graceful.stopped)
2075
+ return { ...graceful, trackedProcessIdentities };
2076
+ for (const pid of trackedPids) {
2077
+ const probe = probeExactPaneProcess(paneId, authorization.pid, pid);
2078
+ if (probe.status === 'unavailable')
2079
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: probe.proof };
2080
+ if (probe.status === 'stopped')
2081
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
2082
+ if (probe.status === 'gone')
2083
+ continue;
2084
+ if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
2085
+ return {
2086
+ terminated: false,
2087
+ stopped: true,
2088
+ trackedPids,
2089
+ trackedProcessIdentities,
2090
+ authorizedPanePid: authorization.pid,
2091
+ proofUnavailable: {
2092
+ status: 'unavailable',
2093
+ paneId,
2094
+ reason: 'pane_pid_changed',
2095
+ detail: 'pane owner authorization changed',
2096
+ },
2097
+ };
2098
+ }
2099
+ if (!await revalidateDescendantIdentity(pid)) {
2100
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
2101
+ }
2102
+ const signal = signalExactPaneProcess(paneId, authorization.pid, pid, 'SIGKILL');
2103
+ if (signal.status === 'unavailable')
2104
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: signal.proof };
2105
+ if (signal.status === 'stopped' || signal.status === 'unknown')
2106
+ return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
2107
+ }
2108
+ return { ...(await waitForExactPaneTrackedPidsExit(paneId, authorization.pid, trackedPids, killWaitMs)), trackedProcessIdentities };
2109
+ }
2110
+ function gonePaneDescendantCleanupDebtPath(teamName, cwd, config) {
2111
+ const stateRoot = config?.team_state_root ?? resolveCanonicalTeamStateRoot(resolve(cwd));
2112
+ return join(stateRoot, 'team', teamName, '.gone-pane-descendant-cleanup-debt.json');
2113
+ }
2114
+ function trackedPidsFromGonePaneDebtEntry(entry) {
2115
+ return 'tracked_pids' in entry
2116
+ ? entry.tracked_pids
2117
+ : entry.tracked_processes.map((identity) => identity.pid);
2118
+ }
2119
+ async function persistGonePaneDescendantCleanupDebt(params) {
2120
+ const { teamName, cwd, config, paneId, teardown } = params;
2121
+ const unresolvedTrackedPids = teardown.trackedPids.filter((pid) => probePidLiveness(pid) !== 'gone');
2122
+ if (unresolvedTrackedPids.length === 0 || typeof teardown.authorizedPanePid !== 'number')
2123
+ return;
2124
+ const liveTrackedProcesses = (teardown.trackedProcessIdentities ?? []).filter((identity) => (unresolvedTrackedPids.includes(identity.pid)));
2125
+ const debtPath = gonePaneDescendantCleanupDebtPath(teamName, cwd, config);
2126
+ let prior = { schema_version: 1, operation: 'gone_pane_descendant_cleanup', entries: [] };
2127
+ try {
2128
+ prior = JSON.parse(await readFile(debtPath, 'utf8'));
2129
+ }
2130
+ catch (error) {
2131
+ if (error.code !== 'ENOENT')
2132
+ throw new Error('gone_pane_descendant_cleanup_debt_unreadable');
2133
+ }
2134
+ if (!isValidGonePaneDescendantCleanupDebt(prior))
2135
+ throw new Error('gone_pane_descendant_cleanup_debt_malformed');
2136
+ const priorEntry = prior.entries.find((entry) => entry.pane_id === paneId);
2137
+ const entries = prior.entries.filter((entry) => entry.pane_id !== paneId);
2138
+ if (liveTrackedProcesses.length !== unresolvedTrackedPids.length) {
2139
+ const priorIdentities = priorEntry?.tracked_processes ?? [];
2140
+ const identitiesByPid = new Map(priorIdentities.map((identity) => [identity.pid, identity]));
2141
+ for (const identity of liveTrackedProcesses) {
2142
+ if (!identitiesByPid.has(identity.pid))
2143
+ identitiesByPid.set(identity.pid, identity);
2144
+ }
2145
+ entries.push({
2146
+ pane_id: paneId,
2147
+ authorized_pane_pid: teardown.authorizedPanePid,
2148
+ tracked_pids: [...new Set([...(priorEntry ? trackedPidsFromGonePaneDebtEntry(priorEntry) : []), ...unresolvedTrackedPids])],
2149
+ ...(identitiesByPid.size > 0 ? { tracked_processes: [...identitiesByPid.values()] } : {}),
2150
+ evidence: 'process_identity_unavailable',
2151
+ });
2152
+ }
2153
+ else {
2154
+ entries.push({
2155
+ pane_id: paneId,
2156
+ authorized_pane_pid: teardown.authorizedPanePid,
2157
+ tracked_processes: liveTrackedProcesses,
2158
+ evidence: teardown.proofUnavailable
2159
+ ? `${teardown.proofUnavailable.reason}:${teardown.proofUnavailable.detail ?? ''}`
2160
+ : 'pane_authority_lost_during_descendant_teardown',
2161
+ });
2162
+ }
2163
+ await writeAtomic(debtPath, JSON.stringify({ schema_version: 1, operation: 'gone_pane_descendant_cleanup', entries }, null, 2));
2164
+ }
2165
+ function isValidProcessIdentity(value) {
2166
+ return typeof value === 'object' && value !== null
2167
+ && Number.isSafeInteger(value.pid)
2168
+ && value.pid > 0
2169
+ && typeof value.start_time === 'string'
2170
+ && /^[0-9]+$/.test(value.start_time);
2171
+ }
2172
+ function isValidGonePaneDescendantCleanupDebt(value) {
2173
+ if (typeof value !== 'object' || value === null)
2174
+ return false;
2175
+ const debt = value;
2176
+ if (debt.schema_version !== 1 || debt.operation !== 'gone_pane_descendant_cleanup' || !Array.isArray(debt.entries))
2177
+ return false;
2178
+ if (new Set(debt.entries.map((entry) => entry?.pane_id)).size !== debt.entries.length)
2179
+ return false;
2180
+ return debt.entries.every((entry) => {
2181
+ if (typeof entry !== 'object' || entry === null)
2182
+ return false;
2183
+ const candidate = entry;
2184
+ if (typeof candidate.pane_id !== 'string' || !/^%[0-9]+$/.test(candidate.pane_id)
2185
+ || !Number.isSafeInteger(candidate.authorized_pane_pid)
2186
+ || candidate.authorized_pane_pid <= 0)
2187
+ return false;
2188
+ if (candidate.evidence === 'process_identity_unavailable') {
2189
+ const trackedPids = candidate.tracked_pids;
2190
+ if (!Array.isArray(candidate.tracked_pids) || candidate.tracked_pids.length === 0
2191
+ || trackedPids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)
2192
+ || new Set(trackedPids).size !== trackedPids.length)
2193
+ return false;
2194
+ if (candidate.tracked_processes === undefined)
2195
+ return true;
2196
+ if (!Array.isArray(candidate.tracked_processes) || !candidate.tracked_processes.every(isValidProcessIdentity))
2197
+ return false;
2198
+ const trackedProcesses = candidate.tracked_processes;
2199
+ return trackedProcesses.every((identity) => trackedPids.includes(identity.pid))
2200
+ && new Set(trackedProcesses.map((identity) => identity.pid)).size === trackedProcesses.length;
2201
+ }
2202
+ if (typeof candidate.evidence !== 'string' || candidate.evidence.length === 0
2203
+ || candidate.tracked_pids !== undefined
2204
+ || !Array.isArray(candidate.tracked_processes) || candidate.tracked_processes.length === 0
2205
+ || !candidate.tracked_processes.every(isValidProcessIdentity))
2206
+ return false;
2207
+ const trackedProcesses = candidate.tracked_processes;
2208
+ return new Set(trackedProcesses.map((identity) => identity.pid)).size === trackedProcesses.length;
2209
+ });
2210
+ }
2211
+ async function reconcileGonePaneDescendantCleanupDebt(teamName, cwd, config) {
2212
+ const debtPath = gonePaneDescendantCleanupDebtPath(teamName, cwd, config);
2213
+ if (!existsSync(debtPath))
2214
+ return;
2215
+ let debt;
2216
+ try {
2217
+ debt = JSON.parse(await readFile(debtPath, 'utf8'));
2218
+ }
2219
+ catch {
2220
+ throw new Error('gone_pane_descendant_cleanup_debt_unreadable');
2221
+ }
2222
+ if (!isValidGonePaneDescendantCleanupDebt(debt))
2223
+ throw new Error('gone_pane_descendant_cleanup_debt_malformed');
2224
+ const unresolved = [];
2225
+ for (const entry of debt.entries) {
2226
+ if ('tracked_pids' in entry) {
2227
+ const trackedProcesses = entry.tracked_processes ?? [];
2228
+ const knownIdentities = new Map(trackedProcesses.map((identity) => [identity.pid, identity]));
2229
+ const states = await Promise.all(entry.tracked_pids.map((pid) => {
2230
+ const identity = knownIdentities.get(pid);
2231
+ return identity ? probeProcessIdentity(identity) : Promise.resolve(probePidLiveness(pid) === 'gone' ? 'gone' : 'reused_or_unknown');
2232
+ }));
2233
+ if (states.some((state) => state !== 'gone'))
2234
+ unresolved.push(entry);
2235
+ continue;
2236
+ }
2237
+ const states = await Promise.all(entry.tracked_processes.map(probeProcessIdentity));
2238
+ if (states.some((state) => state !== 'gone'))
2239
+ unresolved.push(entry);
2240
+ }
2241
+ if (unresolved.length === 0) {
2242
+ await removeDurableFile(debtPath);
2243
+ return;
2244
+ }
2245
+ await writeAtomic(debtPath, JSON.stringify({ ...debt, entries: unresolved }, null, 2));
2246
+ throw new Error(`gone_pane_descendant_cleanup_debt_unresolved:${unresolved.map((entry) => entry.pane_id).join(',')}`);
2247
+ }
1606
2248
  async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, context) {
1607
2249
  const handle = getPromptWorkerHandle(teamName, workerName);
1608
2250
  const handlePid = handle?.pid;
@@ -1615,7 +2257,9 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
1615
2257
  return { terminated: true, forcedKill: false, pid: null };
1616
2258
  }
1617
2259
  const teardown = await terminateTrackedProcessTree(pid ?? 0, processGroupId);
1618
- const processGone = processGroupId ? !isProcessGroupAlive(processGroupId) : !isPidAlive(pid);
2260
+ const processGone = processGroupId
2261
+ ? probeProcessGroupLiveness(processGroupId) === 'gone'
2262
+ : isPidGone(pid);
1619
2263
  if (teardown.terminated && processGone) {
1620
2264
  removePromptWorkerHandle(teamName, workerName);
1621
2265
  return { terminated: true, forcedKill: teardown.forcedKill, pid };
@@ -1625,31 +2269,27 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
1625
2269
  worker: workerName,
1626
2270
  reason: `prompt_force_kill:${context}:pid=${pid}`,
1627
2271
  }, cwd).catch(() => { });
1628
- if (!teardown.terminated) {
1629
- await appendTeamEvent(teamName, {
1630
- type: 'worker_stopped',
1631
- worker: workerName,
1632
- reason: `prompt_teardown_failed:${context}:pid=${pid}`,
1633
- }, cwd).catch(() => { });
1634
- return {
1635
- terminated: false,
1636
- forcedKill: teardown.forcedKill,
1637
- pid,
1638
- error: 'still_alive_after_sigkill',
1639
- };
1640
- }
1641
- removePromptWorkerHandle(teamName, workerName);
1642
- return { terminated: true, forcedKill: teardown.forcedKill, pid };
2272
+ await appendTeamEvent(teamName, {
2273
+ type: 'worker_stopped',
2274
+ worker: workerName,
2275
+ reason: `prompt_teardown_failed:${context}:pid=${pid}`,
2276
+ }, cwd).catch(() => { });
2277
+ return {
2278
+ terminated: false,
2279
+ forcedKill: teardown.forcedKill,
2280
+ pid,
2281
+ error: 'still_alive_after_sigkill',
2282
+ };
1643
2283
  }
1644
2284
  function isPromptWorkerAlive(config, worker) {
1645
2285
  const handle = getPromptWorkerHandle(config.name, worker.name);
1646
2286
  if (handle?.child.exitCode === null && !handle.child.killed)
1647
2287
  return true;
1648
- if (handle?.processGroupId && isProcessGroupAlive(handle.processGroupId))
2288
+ if (handle?.processGroupId && probeProcessGroupLiveness(handle.processGroupId) !== 'gone')
1649
2289
  return true;
1650
- if (process.platform !== 'win32' && isProcessGroupAlive(worker.pid))
2290
+ if (process.platform !== 'win32' && probeProcessGroupLiveness(worker.pid) !== 'gone')
1651
2291
  return true;
1652
- return isPidAlive(worker.pid);
2292
+ return probePidLiveness(worker.pid) !== 'gone';
1653
2293
  }
1654
2294
  export { TEAM_LOW_COMPLEXITY_DEFAULT_MODEL };
1655
2295
  export { resolveCanonicalTeamStateRoot };
@@ -1741,13 +2381,40 @@ function resolveEffectiveWorkerCliForStartupLog(resolvedLaunchArgs, env) {
1741
2381
  }
1742
2382
  return resolveTeamWorkerCli(resolvedLaunchArgs, env);
1743
2383
  }
2384
+ function buildWorkerLaunchPolicyPlan(params) {
2385
+ const plans = [];
2386
+ for (let workerIndex = 1; workerIndex <= params.workerCount; workerIndex++) {
2387
+ const workerName = `worker-${workerIndex}`;
2388
+ const taskRoles = params.tasks
2389
+ .filter((task) => task.owner === workerName)
2390
+ .map((task) => task.role)
2391
+ .filter((role) => Boolean(role));
2392
+ const uniqueTaskRoles = [...new Set(taskRoles)];
2393
+ const workerRole = taskRoles.length > 0 && uniqueTaskRoles.length === 1
2394
+ ? taskRoles[0]
2395
+ : params.agentType;
2396
+ const preferredReasoning = resolveAgentReasoningEffort(workerRole, params.codexHomeOverride)
2397
+ ?? resolveAgentReasoningEffort(params.agentType, params.codexHomeOverride);
2398
+ const workerLaunchArgs = resolveWorkerLaunchArgsFromEnv(params.launchEnv, workerRole, undefined, preferredReasoning);
2399
+ const workerCli = resolveTeamWorkerCliForResolvedLaunchArgs(workerIndex, params.workerCount, workerLaunchArgs, params.launchEnv);
2400
+ plans.push(Object.freeze({
2401
+ workerName,
2402
+ workerRole,
2403
+ taskRoles: Object.freeze(uniqueTaskRoles),
2404
+ preferredReasoning,
2405
+ workerLaunchArgs: Object.freeze([...workerLaunchArgs]),
2406
+ workerCli,
2407
+ }));
2408
+ }
2409
+ return Object.freeze(plans);
2410
+ }
1744
2411
  async function writeDecompositionArtifacts(teamName, cwd, metadata) {
1745
2412
  const root = join(resolveCanonicalTeamStateRoot(cwd), 'team', teamName);
1746
2413
  await mkdir(root, { recursive: true });
1747
2414
  await writeFile(join(root, 'decomposition-report.json'), JSON.stringify(metadata, null, 2), 'utf8');
1748
2415
  const manifest = await readTeamManifestV2(teamName, cwd);
1749
2416
  if (manifest) {
1750
- await writeFile(join(root, 'manifest.v2.json'), JSON.stringify({ ...manifest, team_decomposition: metadata }, null, 2), 'utf8');
2417
+ await writeTeamManifestV2State({ ...manifest, team_decomposition: { ...metadata } }, cwd);
1751
2418
  }
1752
2419
  }
1753
2420
  export async function settleStartupAttemptResults(attempts) {
@@ -1769,13 +2436,14 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1769
2436
  : (typeof process.env.CODEX_HOME === 'string' && process.env.CODEX_HOME.trim() !== ''
1770
2437
  ? process.env.CODEX_HOME.trim()
1771
2438
  : undefined);
1772
- const launchEnv = codexHomeOverride
1773
- ? { ...process.env, CODEX_HOME: codexHomeOverride }
1774
- : process.env;
2439
+ const launchEnv = {
2440
+ ...process.env,
2441
+ ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
2442
+ };
2443
+ const workerLaunchMode = resolveTeamWorkerLaunchMode(launchEnv);
1775
2444
  await assertNestedTeamAllowed(leaderCwd);
1776
2445
  const effectiveWorktreeMode = resolveEffectiveTeamWorktreeMode(leaderCwd, options.worktreeMode);
1777
2446
  const displayName = sanitizeTeamName(teamName);
1778
- const workerLaunchMode = resolveTeamWorkerLaunchMode(launchEnv);
1779
2447
  const displayMode = workerLaunchMode === 'interactive' ? 'split_pane' : 'auto';
1780
2448
  const rawIdentityScope = resolveTeamIdentityScope(launchEnv);
1781
2449
  const resolvedLeaderSessionId = await resolveLeaderSessionId(leaderCwd, launchEnv);
@@ -1792,18 +2460,12 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1792
2460
  : rawIdentityScope;
1793
2461
  const sanitized = buildInternalTeamName(displayName, identityScope);
1794
2462
  const leaderSessionId = identityScope.sessionId || identityScope.paneId || identityScope.tmuxTarget || identityScope.runId;
2463
+ const existingStartupConfig = await readTeamConfig(sanitized, leaderCwd);
2464
+ await reconcileGonePaneDescendantCleanupDebt(sanitized, leaderCwd, existingStartupConfig ?? undefined);
1795
2465
  await assertTeamStartupIsNonDestructive(sanitized, leaderCwd, leaderSessionId);
1796
2466
  if (displayName !== sanitized) {
1797
2467
  await assertTeamStartupIsNonDestructive(displayName, leaderCwd, leaderSessionId);
1798
2468
  }
1799
- if (workerLaunchMode === 'interactive') {
1800
- if (!isTmuxAvailable()) {
1801
- throw new Error('Team mode requires tmux. Install with: apt install tmux / brew install tmux');
1802
- }
1803
- if (!hasCurrentTmuxClientContext()) {
1804
- throw new Error('Team mode requires running inside tmux current leader pane');
1805
- }
1806
- }
1807
2469
  const teamStateRoot = resolveCanonicalTeamStateRoot(leaderCwd);
1808
2470
  const requestedApprovedExecution = normalizeApprovedTeamExecutionBinding(options.approvedExecution);
1809
2471
  const requestedApprovedExecutionOutcome = requestedApprovedExecution
@@ -1837,9 +2499,32 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1837
2499
  for (let i = 1; i <= workerCount; i++) {
1838
2500
  workerWorkspaceByName.set(`worker-${i}`, { cwd: leaderCwd });
1839
2501
  }
1840
- await detectAndCleanStaleTeam(sanitized, leaderCwd, workerCount, options.confirmStaleCleanup);
1841
2502
  if (activeWorktreeMode) {
1842
2503
  assertCleanLeaderWorkspaceForWorkerWorktrees(leaderCwd);
2504
+ }
2505
+ const workerLaunchPolicyPlan = buildWorkerLaunchPolicyPlan({
2506
+ workerCount,
2507
+ tasks,
2508
+ agentType,
2509
+ launchEnv,
2510
+ codexHomeOverride,
2511
+ });
2512
+ for (const workerLaunchPolicy of workerLaunchPolicyPlan) {
2513
+ assertTeamWorkerCliPolicyCompatibility(workerLaunchPolicy.workerCli, [...workerLaunchPolicy.workerLaunchArgs]);
2514
+ }
2515
+ if (workerLaunchMode === 'prompt') {
2516
+ assertPromptModeWorkerCliSupported(workerLaunchPolicyPlan.map((workerLaunchPolicy) => workerLaunchPolicy.workerCli), launchEnv);
2517
+ }
2518
+ if (workerLaunchMode === 'interactive') {
2519
+ if (!isTmuxAvailable()) {
2520
+ throw new Error('Team mode requires tmux. Install with: apt install tmux / brew install tmux');
2521
+ }
2522
+ if (!hasCurrentTmuxClientContext()) {
2523
+ throw new Error('Team mode requires running inside tmux current leader pane');
2524
+ }
2525
+ }
2526
+ await detectAndCleanStaleTeam(sanitized, leaderCwd, workerCount, options.confirmStaleCleanup);
2527
+ if (activeWorktreeMode) {
1843
2528
  for (let i = 1; i <= workerCount; i++) {
1844
2529
  const workerName = `worker-${i}`;
1845
2530
  const planned = planWorktreeTarget({
@@ -1870,11 +2555,18 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1870
2555
  let sessionCreated = false;
1871
2556
  const createdWorkerPaneIds = [];
1872
2557
  let createdLeaderPaneId;
2558
+ // A returned interactive session has successfully tagged these panes. Rollback
2559
+ // must retain that ownership authorization through its final kill proof.
2560
+ const startupTaggedPaneOwnerIds = new Map();
2561
+ const authorizeStartupRollbackPaneEffect = (paneId) => {
2562
+ const expectedOwnerId = startupTaggedPaneOwnerIds.get(paneId);
2563
+ if (!expectedOwnerId)
2564
+ return true;
2565
+ const currentOwner = readPaneTeamOwnerTagResult(paneId);
2566
+ return currentOwner.status === 'value' && currentOwner.value === expectedOwnerId;
2567
+ };
1873
2568
  let config = null;
1874
- const sharedWorkerLaunchArgs = resolveTeamWorkerLaunchArgs({
1875
- existingRaw: launchEnv.OMX_TEAM_WORKER_LAUNCH_ARGS,
1876
- fallbackModel: resolveAgentDefaultModel(agentType, codexHomeOverride),
1877
- });
2569
+ const workerPaneIds = Array.from({ length: workerCount }, () => undefined);
1878
2570
  const workerReadyTimeoutMs = resolveWorkerReadyTimeoutMs(launchEnv);
1879
2571
  const workerStartupEvidenceTimeoutMs = resolveWorkerStartupEvidenceTimeoutMs(launchEnv, workerReadyTimeoutMs);
1880
2572
  const startupDispatchRetries = resolveStartupDispatchRetries(launchEnv);
@@ -1970,24 +2662,20 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1970
2662
  await writeDecompositionArtifacts(sanitized, leaderCwd, effectiveDecompositionMetadata);
1971
2663
  }
1972
2664
  const workerBootstrapPlans = [];
1973
- const workerCliPlan = [];
1974
2665
  for (let i = 1; i <= workerCount; i++) {
1975
- const workerName = `worker-${i}`;
2666
+ const workerLaunchPolicy = workerLaunchPolicyPlan[i - 1];
2667
+ if (!workerLaunchPolicy) {
2668
+ throw new Error(`missing worker launch policy for worker-${i}`);
2669
+ }
2670
+ const workerName = workerLaunchPolicy.workerName;
1976
2671
  const workerWorkspace = workerWorkspaceByName.get(workerName) ?? { cwd: leaderCwd };
1977
2672
  const workerTasks = allTasks.filter(t => t.owner === workerName);
1978
- const taskRoles = workerTasks.map(t => t.role).filter(Boolean);
1979
- const uniqueTaskRoles = new Set(taskRoles);
1980
- const workerRole = taskRoles.length > 0 && uniqueTaskRoles.size === 1
1981
- ? taskRoles[0]
1982
- : agentType;
1983
- const runtimeRole = workerRole;
2673
+ const runtimeRole = workerLaunchPolicy.workerRole;
1984
2674
  const rawRolePromptContent = await loadRolePrompt(runtimeRole, join(leaderCwd, '.codex', 'prompts'))
1985
2675
  ?? await loadRolePrompt(runtimeRole, codexPromptsDir());
1986
- const preferredReasoning = resolveAgentReasoningEffort(runtimeRole, codexHomeOverride)
1987
- ?? resolveAgentReasoningEffort(agentType, codexHomeOverride);
1988
- const workerLaunchArgs = resolveWorkerLaunchArgsFromEnv(launchEnv, runtimeRole, undefined, preferredReasoning);
1989
- const workerCli = resolveTeamWorkerCliForResolvedLaunchArgs(i, workerCount, workerLaunchArgs, launchEnv);
1990
- const resolvedWorkerModel = parseTeamWorkerLaunchArgs(workerLaunchArgs).modelOverride ?? undefined;
2676
+ const workerLaunchArgs = workerLaunchPolicy.workerLaunchArgs;
2677
+ const workerCli = workerLaunchPolicy.workerCli;
2678
+ const resolvedWorkerModel = parseTeamWorkerLaunchArgs([...workerLaunchArgs]).modelOverride ?? undefined;
1991
2679
  const rolePromptContent = rawRolePromptContent
1992
2680
  ? composeRoleInstructionsForRole(runtimeRole, rawRolePromptContent, resolvedWorkerModel)
1993
2681
  : null;
@@ -2033,12 +2721,11 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2033
2721
  if (initialPrompt) {
2034
2722
  await writeWorkerInbox(sanitized, workerName, inbox, leaderCwd);
2035
2723
  }
2036
- workerCliPlan.push(workerCli);
2037
2724
  workerBootstrapPlans.push({
2038
2725
  workerName,
2039
2726
  workerWorkspace,
2040
2727
  workerTasks,
2041
- workerRole,
2728
+ workerRole: runtimeRole,
2042
2729
  rolePromptContent,
2043
2730
  instructionsFilePath,
2044
2731
  inbox,
@@ -2050,9 +2737,6 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2050
2737
  toolContext,
2051
2738
  });
2052
2739
  }
2053
- if (workerLaunchMode === 'prompt') {
2054
- assertPromptModeWorkerCliSupported(workerCliPlan);
2055
- }
2056
2740
  const workerStartups = workerBootstrapPlans.map((plan) => {
2057
2741
  const env = {
2058
2742
  [TEAM_STATE_ROOT_ENV]: teamStateRoot,
@@ -2075,12 +2759,11 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2075
2759
  cwd: plan.workerWorkspace.cwd,
2076
2760
  env,
2077
2761
  initialPrompt: plan.initialPrompt,
2078
- launchArgs: plan.workerLaunchArgs,
2762
+ launchArgs: [...plan.workerLaunchArgs],
2079
2763
  workerCli: plan.workerCli,
2080
2764
  workerRole: plan.workerRole,
2081
2765
  };
2082
2766
  });
2083
- const workerPaneIds = Array.from({ length: workerCount }, () => undefined);
2084
2767
  const materializeWorkerStartupState = async (bootstrapPlan, workerIndex, paneId) => {
2085
2768
  const workerWorkspace = bootstrapPlan.workerWorkspace;
2086
2769
  const identity = {
@@ -2097,13 +2780,19 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2097
2780
  worktree_created: workerWorkspace.worktreeCreated,
2098
2781
  team_state_root: teamStateRoot,
2099
2782
  };
2100
- if (workerLaunchMode === 'interactive') {
2101
- const panePid = getWorkerPanePid(sessionName, workerIndex, paneId);
2102
- if (panePid)
2103
- identity.pid = panePid;
2783
+ const persistedPanePid = config?.workers[workerIndex - 1]?.pid;
2784
+ if (workerLaunchMode === 'interactive' && paneId) {
2785
+ if (typeof persistedPanePid !== 'number' || !Number.isSafeInteger(persistedPanePid) || persistedPanePid <= 0) {
2786
+ throw new Error(`startup_worker_pane_pid_unavailable:${paneId}`);
2787
+ }
2788
+ const paneProof = readExactPaneProofSync(paneId);
2789
+ if (paneProof.status !== 'live' || paneProof.pid !== persistedPanePid) {
2790
+ throw new Error(`startup_worker_pane_identity_changed:${paneId}`);
2791
+ }
2792
+ identity.pid = persistedPanePid;
2104
2793
  }
2105
- else if (config?.workers[workerIndex - 1]?.pid) {
2106
- identity.pid = config.workers[workerIndex - 1].pid;
2794
+ else if (typeof persistedPanePid === 'number') {
2795
+ identity.pid = persistedPanePid;
2107
2796
  }
2108
2797
  if (paneId)
2109
2798
  identity.pane_id = paneId;
@@ -2142,12 +2831,26 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2142
2831
  });
2143
2832
  const startupTiming = createStartupTimingRecorder(sanitized, leaderCwd);
2144
2833
  if (workerLaunchMode === 'interactive') {
2145
- const createdSession = createTeamSession(sanitized, workerCount, leaderCwd, sharedWorkerLaunchArgs, workerStartups, { ownerSessionId: leaderSessionId, teamPaneOwnerId: config.tmux_pane_owner_id });
2834
+ const createdSession = createTeamSession(sanitized, workerCount, leaderCwd, Array.from(workerLaunchPolicyPlan[0]?.workerLaunchArgs ?? []), workerStartups, { ownerSessionId: leaderSessionId, teamPaneOwnerId: config.tmux_pane_owner_id });
2146
2835
  sessionName = createdSession.name;
2147
2836
  sessionCreated = true;
2148
2837
  createdWorkerPaneIds.push(...createdSession.workerPaneIds);
2149
2838
  createdLeaderPaneId = createdSession.leaderPaneId;
2150
2839
  applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds);
2840
+ const createdOwnerId = typeof createdSession.teamPaneOwnerId === 'string'
2841
+ ? createdSession.teamPaneOwnerId.trim()
2842
+ : '';
2843
+ if (createdOwnerId) {
2844
+ for (const paneId of [
2845
+ createdSession.leaderPaneId,
2846
+ ...createdSession.workerPaneIds,
2847
+ createdSession.hudPaneId,
2848
+ ]) {
2849
+ if (typeof paneId === 'string' && paneId.trim().startsWith('%')) {
2850
+ startupTaggedPaneOwnerIds.set(paneId, createdOwnerId);
2851
+ }
2852
+ }
2853
+ }
2151
2854
  for (const [index, paneId] of createdSession.workerPaneIds.entries()) {
2152
2855
  startupTiming.mark('split_returned', { worker: `worker-${index + 1}`, pane_id: paneId });
2153
2856
  }
@@ -2156,12 +2859,17 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2156
2859
  config.tmux_session = `prompt-${sanitized}`;
2157
2860
  config.leader_pane_id = null;
2158
2861
  config.hud_pane_id = null;
2862
+ config.leader_pane_pid = null;
2863
+ config.hud_pane_pid = null;
2159
2864
  config.resize_hook_name = null;
2160
2865
  config.resize_hook_target = null;
2161
2866
  for (let i = 1; i <= workerCount; i++) {
2162
- const startup = workerStartups[i - 1] || {};
2163
- const workerName = `worker-${i}`;
2164
- const child = spawnPromptWorker(sanitized, workerName, i, startup.cwd || leaderCwd, startup.launchArgs || sharedWorkerLaunchArgs, startup.env || {}, startup.workerCli || workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
2867
+ const startup = workerStartups[i - 1];
2868
+ const workerLaunchPolicy = workerLaunchPolicyPlan[i - 1];
2869
+ if (!startup || !workerLaunchPolicy) {
2870
+ throw new Error(`missing worker launch plan for worker-${i}`);
2871
+ }
2872
+ const child = spawnPromptWorker(sanitized, workerLaunchPolicy.workerName, i, startup.cwd, startup.launchArgs, startup.env, startup.workerCli, startup.initialPrompt, startup.workerRole);
2165
2873
  if (config.workers[i - 1]) {
2166
2874
  config.workers[i - 1].pid = child.pid;
2167
2875
  }
@@ -2202,12 +2910,9 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2202
2910
  const triggerIntent = bootstrapPlan.triggerIntent;
2203
2911
  const initialPrompt = bootstrapPlan.initialPrompt;
2204
2912
  const startupStartedAt = performance.now();
2205
- const taskRoles = workerTasks
2206
- .map((task) => task.role)
2207
- .filter((role) => Boolean(role));
2208
- const uniqueTaskRoles = [...new Set(taskRoles)];
2209
- if (uniqueTaskRoles.length > 1) {
2210
- console.log(`[omx:team] ${workerName}: mixed task roles [${uniqueTaskRoles.join(', ')}], falling back to ${agentType}`);
2913
+ const taskRoles = workerLaunchPolicyPlan[workerIndex - 1]?.taskRoles ?? [];
2914
+ if (taskRoles.length > 1) {
2915
+ console.log(`[omx:team] ${workerName}: mixed task roles [${taskRoles.join(', ')}], falling back to ${agentType}`);
2211
2916
  }
2212
2917
  const startupDirectOutcome = workerLaunchMode === 'interactive' && !initialPrompt
2213
2918
  ? await attemptStartupDirectTrigger({
@@ -2216,7 +2921,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2216
2921
  workerName,
2217
2922
  workerIndex,
2218
2923
  paneId,
2219
- workerCli: workerCliPlan[workerIndex - 1],
2924
+ workerCli: bootstrapPlan.workerCli,
2220
2925
  inbox,
2221
2926
  triggerMessage: trigger,
2222
2927
  intent: triggerIntent,
@@ -2228,10 +2933,10 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2228
2933
  let startupReadyPromptObserved = false;
2229
2934
  if (workerLaunchMode === 'interactive' && !skipWorkerReadyWait && !initialPrompt && !startupDirectOutcome?.ok) {
2230
2935
  startupTiming.mark('ready_wait_start', { worker: workerName, pane_id: paneId });
2231
- const ready = await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId);
2936
+ const ready = await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
2232
2937
  startupTiming.mark('ready_wait_end', { worker: workerName, pane_id: paneId, ok: ready });
2233
2938
  if (!ready) {
2234
- const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId);
2939
+ const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
2235
2940
  if (workerAlive) {
2236
2941
  await recordRecoverableStartupIssue({
2237
2942
  teamName: sanitized,
@@ -2265,7 +2970,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2265
2970
  workerName,
2266
2971
  workerIndex,
2267
2972
  paneId,
2268
- workerCli: workerCliPlan[workerIndex - 1],
2973
+ workerCli: bootstrapPlan.workerCli,
2269
2974
  inbox,
2270
2975
  triggerMessage: trigger,
2271
2976
  intent: triggerIntent,
@@ -2292,8 +2997,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2292
2997
  break;
2293
2998
  if (attempt < startupDispatchRetries) {
2294
2999
  if (workerLaunchMode === 'interactive') {
2295
- if (dismissTrustPromptIfPresent(sessionName, workerIndex, paneId)) {
2296
- await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId);
3000
+ if (dismissTrustPromptIfPresent(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
3001
+ await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
2297
3002
  }
2298
3003
  else {
2299
3004
  await new Promise((resolve) => setTimeout(resolve, Math.max(0, startupRetryDelayS * 1000)));
@@ -2308,7 +3013,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2308
3013
  if (!dispatchOutcome.ok) {
2309
3014
  const workerAlive = workerLaunchMode === 'prompt'
2310
3015
  ? isPromptWorkerAlive(config, config.workers[workerIndex - 1])
2311
- : isWorkerPaneOpen(sessionName, workerIndex, paneId);
3016
+ : isWorkerPaneOpen(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
2312
3017
  if (workerLaunchMode === 'prompt' && !workerAlive) {
2313
3018
  await recordPromptStartupWorkerStopped({
2314
3019
  teamName: sanitized,
@@ -2356,6 +3061,52 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2356
3061
  }
2357
3062
  catch (error) {
2358
3063
  const rollbackErrors = [];
3064
+ let preserveRollbackState = false;
3065
+ if (error instanceof CreateTeamSessionPartialError && config) {
3066
+ const partialSession = error.partialSession;
3067
+ sessionName = partialSession.name;
3068
+ sessionCreated = true;
3069
+ createdWorkerPaneIds.push(...partialSession.workerPaneIds);
3070
+ createdLeaderPaneId = partialSession.leaderPaneId;
3071
+ applyCreatedInteractiveSessionToConfig(config, partialSession, workerPaneIds);
3072
+ await saveTeamConfig(config, leaderCwd);
3073
+ const cleanupDebt = [
3074
+ ...error.cleanupErrors,
3075
+ ...error.proofUnavailable.map((proof) => `pane_proof_unavailable:${proof.paneId}:${proof.reason}`),
3076
+ ];
3077
+ if (cleanupDebt.length > 0) {
3078
+ await appendTeamEvent(sanitized, {
3079
+ type: 'team_leader_nudge',
3080
+ worker: 'leader-fixed',
3081
+ reason: `startup_create_session_cleanup_debt:${cleanupDebt.join(';')}; retry preserved session resources`,
3082
+ }, leaderCwd).catch(() => { });
3083
+ }
3084
+ // CreateTeamSessionPartialError means create-time teardown did not reach a
3085
+ // proven-gone state. Preserve its config, hook registration, state and
3086
+ // worktrees exactly as saved above for a later retry; generic rollback
3087
+ // would otherwise destroy the retry evidence.
3088
+ assertPaneTeardownProofsAvailable('startup_rollback', error.proofUnavailable);
3089
+ throw error;
3090
+ }
3091
+ if (config && error instanceof Error && error.message.startsWith('startup_worker_pane_identity_changed:')) {
3092
+ const startupCleanupDebt = config.workers.flatMap((worker) => {
3093
+ if (!worker.pane_id || typeof worker.pid !== 'number' || !Number.isSafeInteger(worker.pid) || worker.pid <= 0)
3094
+ return [];
3095
+ const proof = readExactPaneProofSync(worker.pane_id);
3096
+ return proof.status === 'live' && proof.pid !== worker.pid
3097
+ ? [{ status: 'unavailable', paneId: worker.pane_id, reason: 'pane_pid_changed', detail: `expected ${worker.pid}, got ${proof.pid}` }]
3098
+ : [];
3099
+ });
3100
+ if (startupCleanupDebt.length > 0) {
3101
+ await saveTeamConfig(config, leaderCwd);
3102
+ await appendTeamEvent(sanitized, {
3103
+ type: 'team_leader_nudge',
3104
+ worker: 'leader-fixed',
3105
+ reason: `startup_worker_pane_cleanup_debt:${startupCleanupDebt.map((proof) => `${proof.paneId}:${proof.reason}`).join(',')}; retry preserved session resources`,
3106
+ }, leaderCwd).catch(() => { });
3107
+ assertPaneTeardownProofsAvailable('startup_rollback', startupCleanupDebt);
3108
+ }
3109
+ }
2359
3110
  if (sessionCreated) {
2360
3111
  if (config?.resize_hook_name && config.resize_hook_target) {
2361
3112
  try {
@@ -2368,44 +3119,87 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2368
3119
  rollbackErrors.push(`unregisterResizeHook: ${String(cleanupError)}`);
2369
3120
  }
2370
3121
  }
2371
- if (config) {
2372
- config.resize_hook_name = null;
2373
- config.resize_hook_target = null;
2374
- try {
2375
- await saveTeamConfig(config, leaderCwd);
2376
- }
2377
- catch (cleanupError) {
2378
- rollbackErrors.push(`saveTeamConfig(clear resize hook): ${String(cleanupError)}`);
2379
- }
2380
- }
2381
3122
  // In split-pane topology, we must not kill the entire tmux session; kill only created panes.
2382
3123
  if (sessionName.includes(':')) {
2383
- for (const [index, paneId] of createdWorkerPaneIds.entries()) {
2384
- const panePid = getWorkerPanePid(sessionName, index + 1, paneId);
2385
- if (panePid) {
2386
- await terminateTrackedProcessTree(panePid);
2387
- }
2388
- try {
2389
- await killWorkerByPaneIdAsync(paneId, createdLeaderPaneId);
3124
+ const paneProcessProofUnavailable = [];
3125
+ for (const paneId of createdWorkerPaneIds) {
3126
+ const teardown = await terminateExactPaneProcessTree(paneId, config?.workers.find((worker) => worker.pane_id === paneId)?.pid, undefined, undefined, authorizeStartupRollbackPaneEffect);
3127
+ if (teardown.proofUnavailable) {
3128
+ if (config)
3129
+ await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd: leaderCwd, config, paneId, teardown });
3130
+ paneProcessProofUnavailable.push(teardown.proofUnavailable);
3131
+ break;
2390
3132
  }
2391
- catch (err) {
2392
- process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
3133
+ if (!teardown.terminated && teardown.stopped) {
3134
+ if (config)
3135
+ await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd: leaderCwd, config, paneId, teardown });
3136
+ paneProcessProofUnavailable.push({
3137
+ status: 'unavailable',
3138
+ paneId,
3139
+ reason: 'pane_proof_lost_during_process_teardown',
3140
+ detail: 'exact pane authority was lost before the tracked process tree was resolved',
3141
+ });
3142
+ break;
2393
3143
  }
2394
3144
  }
2395
- if (config?.hud_pane_id) {
2396
- try {
2397
- await killWorkerByPaneIdAsync(config.hud_pane_id, createdLeaderPaneId);
3145
+ if (paneProcessProofUnavailable.length > 0) {
3146
+ if (config)
3147
+ await saveTeamConfig(config, leaderCwd);
3148
+ assertPaneTeardownProofsAvailable('startup_rollback', paneProcessProofUnavailable);
3149
+ }
3150
+ const rollbackPanePids = Object.fromEntries((config?.workers ?? [])
3151
+ .filter((worker) => typeof worker.pane_id === 'string' && typeof worker.pid === 'number')
3152
+ .map((worker) => [worker.pane_id, worker.pid]));
3153
+ if (config?.hud_pane_id && typeof config.hud_pane_pid === 'number') {
3154
+ rollbackPanePids[config.hud_pane_id] = config.hud_pane_pid;
3155
+ }
3156
+ const rollbackPaneIds = [
3157
+ ...createdWorkerPaneIds,
3158
+ ...(config?.hud_pane_id && typeof config.hud_pane_pid === 'number' ? [config.hud_pane_id] : []),
3159
+ ].filter((paneId) => typeof paneId === 'string' && paneId.trim().startsWith('%'));
3160
+ const rollbackPaneTeardown = await teardownWorkerPanes(rollbackPaneIds, {
3161
+ leaderPaneId: createdLeaderPaneId,
3162
+ expectedPanePids: rollbackPanePids,
3163
+ authorizePaneKill: (paneId) => authorizeStartupRollbackPaneEffect(paneId),
3164
+ });
3165
+ const resolvedRollbackPaneIds = new Set([
3166
+ ...rollbackPaneTeardown.provenGonePaneIds,
3167
+ ...rollbackPaneTeardown.killedPaneIds,
3168
+ ]);
3169
+ if (config && (rollbackPaneTeardown.proofUnavailable.length > 0 || rollbackPaneTeardown.kill.failed > 0)) {
3170
+ config.workers = config.workers.filter((worker) => !worker.pane_id || !resolvedRollbackPaneIds.has(worker.pane_id));
3171
+ config.worker_count = config.workers.length;
3172
+ if (config.hud_pane_id && resolvedRollbackPaneIds.has(config.hud_pane_id)) {
3173
+ config.hud_pane_id = null;
3174
+ config.hud_pane_pid = null;
2398
3175
  }
2399
- catch (err) {
2400
- process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
3176
+ }
3177
+ if (rollbackPaneTeardown.proofUnavailable.length > 0) {
3178
+ if (config) {
3179
+ try {
3180
+ await saveTeamConfig(config, leaderCwd);
3181
+ }
3182
+ catch (cleanupError) {
3183
+ rollbackErrors.push(`saveTeamConfig(preserve rollback state): ${String(cleanupError)}`);
3184
+ }
2401
3185
  }
3186
+ assertPaneTeardownProofsAvailable('startup_rollback', rollbackPaneTeardown.proofUnavailable);
3187
+ }
3188
+ if (rollbackPaneTeardown.kill.failed > 0) {
3189
+ if (config)
3190
+ await saveTeamConfig(config, leaderCwd);
3191
+ const originalMessage = error instanceof Error ? error.message : String(error);
3192
+ throw new Error(`${originalMessage}; rollback encountered errors: paneTeardown:${rollbackPaneTeardown.kill.failedPaneIds.join(',')}`);
2402
3193
  }
2403
3194
  }
2404
3195
  else {
2405
3196
  try {
2406
- destroyTeamSession(sessionName);
3197
+ if (!config)
3198
+ throw new Error('detached_session_destroy_authorization_unavailable:missing_config');
3199
+ await destroyConfiguredDetachedTeamSession(sanitized, leaderCwd, config);
2407
3200
  }
2408
3201
  catch (cleanupError) {
3202
+ preserveRollbackState = true;
2409
3203
  rollbackErrors.push(`destroyTeamSession: ${String(cleanupError)}`);
2410
3204
  }
2411
3205
  }
@@ -2443,11 +3237,21 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2443
3237
  }
2444
3238
  }
2445
3239
  restoreTeamModelInstructionsFile(sanitized);
3240
+ let descendantCleanupDebtUnresolved = false;
2446
3241
  try {
2447
- await cleanupTeamState(sanitized, leaderCwd);
3242
+ await reconcileGonePaneDescendantCleanupDebt(sanitized, leaderCwd, config ?? undefined);
2448
3243
  }
2449
3244
  catch (cleanupError) {
2450
- rollbackErrors.push(`cleanupTeamState: ${String(cleanupError)}`);
3245
+ descendantCleanupDebtUnresolved = true;
3246
+ rollbackErrors.push(`reconcileGonePaneDescendantCleanupDebt: ${String(cleanupError)}`);
3247
+ }
3248
+ if (!descendantCleanupDebtUnresolved && !preserveRollbackState) {
3249
+ try {
3250
+ await cleanupTeamState(sanitized, leaderCwd);
3251
+ }
3252
+ catch (cleanupError) {
3253
+ rollbackErrors.push(`cleanupTeamState: ${String(cleanupError)}`);
3254
+ }
2451
3255
  }
2452
3256
  if (provisionedWorktrees.length > 0) {
2453
3257
  try {
@@ -2510,7 +3314,7 @@ export async function monitorTeam(teamName, cwd) {
2510
3314
  const workerSignals = await Promise.all(config.workers.map(async (worker) => {
2511
3315
  const alive = config.worker_launch_mode === 'prompt'
2512
3316
  ? isPromptWorkerAlive(config, worker)
2513
- : isWorkerAlive(sessionName, worker.index, worker.pane_id);
3317
+ : isWorkerAlive(sessionName, worker.index, worker.pane_id, worker.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
2514
3318
  const [status, heartbeat] = await Promise.all([
2515
3319
  readWorkerStatus(sanitized, worker.name, cwd),
2516
3320
  readWorkerHeartbeat(sanitized, worker.name, cwd),
@@ -2760,8 +3564,8 @@ export async function assignTask(teamName, workerName, taskId, cwd) {
2760
3564
  if (outcome.ok)
2761
3565
  break;
2762
3566
  if (attempt < maxAssignRetries && config.worker_launch_mode === 'interactive' && config.tmux_session) {
2763
- if (dismissTrustPromptIfPresent(config.tmux_session, workerInfo.index, workerInfo.pane_id)) {
2764
- waitForWorkerReady(config.tmux_session, workerInfo.index, resolveWorkerReadyTimeoutMs(process.env), workerInfo.pane_id);
3567
+ if (dismissTrustPromptIfPresent(config.tmux_session, workerInfo.index, workerInfo.pane_id, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
3568
+ waitForWorkerReady(config.tmux_session, workerInfo.index, resolveWorkerReadyTimeoutMs(process.env), workerInfo.pane_id, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
2765
3569
  }
2766
3570
  else {
2767
3571
  await new Promise(r => setTimeout(r, assignRetryDelayS * 1000));
@@ -2826,20 +3630,62 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2826
3630
  const confirmIssues = options.confirmIssues === true;
2827
3631
  let skipWorkerAcks = false;
2828
3632
  const sanitized = resolveTeamNameForCurrentContext(teamName, cwd);
2829
- const config = await readTeamConfig(sanitized, cwd);
3633
+ let config = await readTeamConfig(sanitized, cwd);
2830
3634
  if (!config) {
2831
- // No config -- just try to kill tmux session and clean up
2832
- try {
2833
- destroyTeamSession(`omx-team-${sanitized}`);
2834
- }
2835
- catch (err) {
2836
- process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
2837
- }
3635
+ await reconcileDetachedSessionDestroyReceipt(sanitized, cwd, null);
3636
+ await reconcileGonePaneDescendantCleanupDebt(sanitized, cwd);
3637
+ // A missing config is not authority over a conventionally named tmux
3638
+ // session. It may be another team's or a recycled user session.
2838
3639
  await cleanupTeamState(sanitized, cwd);
2839
3640
  await syncTeamModeStateOnShutdown(sanitized, cwd);
2840
3641
  restoreTeamModelInstructionsFile(sanitized);
2841
3642
  return { commitHygieneArtifacts: null };
2842
3643
  }
3644
+ // Reconcile accepted detached-session destruction before any other shutdown
3645
+ // effect. A receipt is the only durable authority across the kill/query crash
3646
+ // window and is deliberately fail-closed on malformed or unavailable input.
3647
+ await reconcileDetachedSessionDestroyReceipt(sanitized, cwd, config);
3648
+ config = await readTeamConfig(sanitized, cwd);
3649
+ if (!config)
3650
+ throw new Error(`shutdown_config_missing_after_detached_reconciliation:${sanitized}`);
3651
+ const restoredHudDebtRoot = join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized);
3652
+ const configuredRestoredHudPaneId = typeof config.hud_pane_id === 'string' && /^%[0-9]+$/.test(config.hud_pane_id)
3653
+ ? config.hud_pane_id
3654
+ : null;
3655
+ const configuredRestoredHudPanePid = typeof config.hud_pane_pid === 'number'
3656
+ && Number.isSafeInteger(config.hud_pane_pid)
3657
+ && config.hud_pane_pid > 0
3658
+ ? config.hud_pane_pid
3659
+ : null;
3660
+ if (configuredRestoredHudPaneId && configuredRestoredHudPanePid) {
3661
+ try {
3662
+ // A restored-HUD debt is finalized only when this canonical config
3663
+ // transaction records its exact frozen pane identity. A stale config may
3664
+ // point to a different HUD after a crash, so replay the pinned debt
3665
+ // rather than treating that mismatch as successful finalization.
3666
+ finalizeRestoredHudCleanupDebtSync(cwd, configuredRestoredHudPaneId, configuredRestoredHudPanePid, restoredHudDebtRoot, true);
3667
+ }
3668
+ catch (error) {
3669
+ if (!(error instanceof Error) || !error.message.startsWith('restored_hud_cleanup_debt_unresolved:')) {
3670
+ throw error;
3671
+ }
3672
+ // The config identity is not the debt identity. Remove it durably before
3673
+ // replaying the pinned obligation: a later detached-session teardown must
3674
+ // not treat a live, same-PID/owner non-HUD pane as the restored HUD.
3675
+ config = await mutateShutdownConfig(config, cwd, (current) => {
3676
+ current.hud_pane_id = null;
3677
+ current.hud_pane_pid = undefined;
3678
+ });
3679
+ reconcileRestoredHudCleanupDebtSync(cwd, restoredHudDebtRoot);
3680
+ }
3681
+ }
3682
+ else {
3683
+ reconcileRestoredHudCleanupDebtSync(cwd, restoredHudDebtRoot);
3684
+ }
3685
+ const priorScaleDownCleanup = await reconcileScaleDownCleanupDebt(sanitized, cwd, config);
3686
+ if (!priorScaleDownCleanup.ok)
3687
+ throw new Error(priorScaleDownCleanup.error);
3688
+ await reconcileGonePaneDescendantCleanupDebt(sanitized, cwd, config);
2843
3689
  const manifest = await readTeamManifestV2(sanitized, cwd);
2844
3690
  const leaderSessionId = typeof manifest?.leader?.session_id === 'string'
2845
3691
  ? manifest.leader.session_id.trim()
@@ -2878,6 +3724,19 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2878
3724
  skipWorkerAcks = true;
2879
3725
  }
2880
3726
  const sessionName = config.tmux_session;
3727
+ const sharedSessionTopology = config.worker_launch_mode === 'interactive' && sessionName.includes(':')
3728
+ ? resolveSharedSessionShutdownTopology(sessionName, config.leader_pane_id, sanitized)
3729
+ : null;
3730
+ if (sharedSessionTopology?.status === 'unavailable') {
3731
+ throw new Error(`shutdown_shared_session_topology_unavailable:${sharedSessionTopology.detail}`);
3732
+ }
3733
+ if (typeof config.hud_pane_id === 'string' && /^%[0-9]+$/.test(config.hud_pane_id)
3734
+ && typeof config.hud_pane_pid === 'number' && Number.isSafeInteger(config.hud_pane_pid) && config.hud_pane_pid > 0) {
3735
+ const persistedHudProof = readExactPaneProofSync(config.hud_pane_id);
3736
+ if (persistedHudProof.status === 'live' && persistedHudProof.pid !== config.hud_pane_pid) {
3737
+ throw new Error(`shutdown_shared_session_HUD_pane_identity_changed:${config.hud_pane_id}`);
3738
+ }
3739
+ }
2881
3740
  const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
2882
3741
  const shutdownRequestTimes = new Map();
2883
3742
  if (!skipWorkerAcks) {
@@ -2933,7 +3792,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2933
3792
  }
2934
3793
  const anyAlive = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
2935
3794
  ? isPromptWorkerAlive(config, w)
2936
- : isWorkerAlive(sessionName, w.index, w.pane_id)));
3795
+ : isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)));
2937
3796
  if (!anyAlive)
2938
3797
  break;
2939
3798
  // Sleep 2s
@@ -2941,7 +3800,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2941
3800
  }
2942
3801
  const anyAliveAfterWait = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
2943
3802
  ? isPromptWorkerAlive(config, w)
2944
- : isWorkerAlive(sessionName, w.index, w.pane_id)));
3803
+ : isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)));
2945
3804
  if (anyAliveAfterWait && !force) {
2946
3805
  // Workers may have accepted shutdown but not exited (Codex TUI requires explicit exit).
2947
3806
  // In this case, proceed to force kill panes (next step) rather than failing and leaving state around.
@@ -2950,15 +3809,13 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2950
3809
  // 3. Force kill remaining workers
2951
3810
  const leaderPaneId = config.leader_pane_id;
2952
3811
  const hudPaneId = config.hud_pane_id;
3812
+ const leaderPanePid = config.leader_pane_pid;
3813
+ const hudPanePid = config.hud_pane_pid;
2953
3814
  if (config.worker_launch_mode === 'interactive') {
2954
- const sharedSessionTopology = sessionName.includes(':')
2955
- ? resolveSharedSessionShutdownTopology(sessionName, leaderPaneId, sanitized)
2956
- : null;
2957
- const effectiveLeaderPaneId = sharedSessionTopology ? sharedSessionTopology.leaderPaneId : leaderPaneId;
3815
+ const effectiveLeaderPaneId = sharedSessionTopology?.status === 'available'
3816
+ ? sharedSessionTopology.leaderPaneId
3817
+ : leaderPaneId;
2958
3818
  const tmuxPaneOwnerId = typeof config.tmux_pane_owner_id === 'string' ? config.tmux_pane_owner_id.trim() : '';
2959
- const legacyPersistedWorkerPaneIds = new Set(config.workers
2960
- .map((worker) => (typeof worker.pane_id === 'string' ? worker.pane_id.trim() : ''))
2961
- .filter((paneId) => paneId.startsWith('%')));
2962
3819
  const ownerReadWarnings = new Set();
2963
3820
  const warnOwnerReadError = (kind, paneId, error) => {
2964
3821
  const key = `${kind}:${paneId}:${error}`;
@@ -2974,57 +3831,253 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2974
3831
  legacyInstanceId: leaderSessionId,
2975
3832
  }) ? effectiveLeaderPaneId : null)
2976
3833
  : effectiveLeaderPaneId;
2977
- const effectiveHudPaneId = sharedSessionTopology
2978
- ? (sharedSessionTopology.hudPaneIds.find((paneId) => isSharedSessionHudPaneReclaimable({
3834
+ const reclaimableHudPaneIds = sharedSessionTopology
3835
+ ? sharedSessionTopology.hudPaneIds.filter((paneId) => isSharedSessionHudPaneReclaimable({
2979
3836
  paneId,
2980
3837
  persistedHudPaneId: hudPaneId,
2981
3838
  leaderOwnedHudPaneIds: sharedSessionTopology.leaderOwnedHudPaneIds,
2982
3839
  teamPaneOwnerId: tmuxPaneOwnerId,
2983
- onOwnerReadError: (hudPaneId, error) => warnOwnerReadError('HUD pane', hudPaneId, error),
2984
- })) ?? null)
2985
- : hudPaneId;
2986
- const shutdownCandidatePaneIds = sharedSessionTopology
2987
- ? filterSharedSessionShutdownWorkerPaneIdsByOwner(sharedSessionTopology.teamWorkerPaneIds, tmuxPaneOwnerId, legacyPersistedWorkerPaneIds, (paneId, error) => warnOwnerReadError('worker pane', paneId, error))
2988
- : listPaneIds(sessionName);
2989
- let shutdownPaneIds = collectShutdownPaneIds({
2990
- config,
2991
- candidatePaneIds: shutdownCandidatePaneIds,
2992
- includePersistedWorkerPaneIds: !sharedSessionTopology,
2993
- leaderPaneId: effectiveLeaderPaneId,
2994
- hudPaneId: effectiveHudPaneId,
2995
- });
2996
- if (shouldPrekillInteractiveShutdownProcessTrees(sessionName)) {
2997
- const workerPanePids = shutdownPaneIds
2998
- .map((paneId) => getWorkerPanePid(sessionName, 1, paneId))
2999
- .filter((pid) => typeof pid === 'number' && Number.isFinite(pid) && pid > 0);
3000
- for (const panePid of workerPanePids) {
3001
- await terminateTrackedProcessTree(panePid);
3840
+ onOwnerReadError: (candidateHudPaneId, error) => {
3841
+ warnOwnerReadError('HUD pane', candidateHudPaneId, error);
3842
+ },
3843
+ }))
3844
+ : (hudPaneId ? [hudPaneId] : []);
3845
+ const effectiveHudPaneId = reclaimableHudPaneIds[0] ?? null;
3846
+ const explicitPersistedWorkerPaneIds = new Set(config.workers
3847
+ .map((worker) => (typeof worker.pane_id === 'string' ? worker.pane_id.trim() : ''))
3848
+ .filter((paneId) => /^%[0-9]+$/.test(paneId))
3849
+ .filter((paneId) => paneId !== leaderPaneId && paneId !== hudPaneId));
3850
+ // Every persisted worker pane is a canonical shutdown member. Command markers
3851
+ // only discover additional candidates; they cannot erase canonical membership.
3852
+ const canonicalExplicitWorkerPaneIds = new Set(explicitPersistedWorkerPaneIds);
3853
+ const initiallyTaggedWorkerPaneIds = new Set();
3854
+ const authorizedDiscoveredWorkerPaneIds = sharedSessionTopology
3855
+ ? collectAuthorizedSharedSessionWorkerPaneIds([...new Set([
3856
+ ...canonicalExplicitWorkerPaneIds,
3857
+ ...sharedSessionTopology.teamWorkerPaneIds,
3858
+ ])], tmuxPaneOwnerId, canonicalExplicitWorkerPaneIds, undefined, initiallyTaggedWorkerPaneIds)
3859
+ : (sessionName ? listPaneIds(sessionName) : []);
3860
+ const excludedSharedWorkerPaneIds = new Set([effectiveLeaderPaneId, effectiveHudPaneId]
3861
+ .filter((paneId) => typeof paneId === 'string' && /^%[0-9]+$/.test(paneId)));
3862
+ const sharedWorkerPaneIds = [...new Set(authorizedDiscoveredWorkerPaneIds)];
3863
+ if (sharedSessionTopology && sharedWorkerPaneIds.some((paneId) => excludedSharedWorkerPaneIds.has(paneId))) {
3864
+ throw new Error(`shutdown_shared_session_worker_target_invalid:${sharedWorkerPaneIds.filter((paneId) => excludedSharedWorkerPaneIds.has(paneId)).join(',')}`);
3865
+ }
3866
+ // Freeze this union before any shared-session topology effect. HUD teardown
3867
+ // and restoration must never cause later worker target rediscovery.
3868
+ const shutdownPaneIds = sharedSessionTopology
3869
+ ? sharedWorkerPaneIds
3870
+ : collectShutdownPaneIds({
3871
+ config,
3872
+ candidatePaneIds: authorizedDiscoveredWorkerPaneIds,
3873
+ leaderPaneId: effectiveLeaderPaneId,
3874
+ hudPaneId: effectiveHudPaneId,
3875
+ });
3876
+ const canonicalWorkerPaneIds = [...shutdownPaneIds];
3877
+ const expectedSharedWorkerPanePids = new Map();
3878
+ const prekillResolvedWorkerPaneIds = new Set();
3879
+ const freezeSharedPaneAuthorization = (paneId, kind, expectedPid) => {
3880
+ const proof = readExactPaneProofSync(paneId);
3881
+ if (proof.status !== 'live') {
3882
+ throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_proof_unavailable:${paneId}`);
3002
3883
  }
3884
+ if (typeof expectedPid === 'number') {
3885
+ if (!Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
3886
+ throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_pid_missing:${paneId}`);
3887
+ }
3888
+ if (proof.pid !== expectedPid) {
3889
+ throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_identity_changed:${paneId}`);
3890
+ }
3891
+ }
3892
+ const owner = readPaneTeamOwnerTagResult(paneId);
3893
+ if (owner.status === 'error') {
3894
+ throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_unavailable:${paneId}:${owner.error}`);
3895
+ }
3896
+ if (owner.status !== 'value' || !tmuxPaneOwnerId || owner.value !== tmuxPaneOwnerId) {
3897
+ throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_changed:${paneId}`);
3898
+ }
3899
+ return { paneId, pid: proof.pid, owner: owner.value };
3900
+ };
3901
+ const assertSharedPaneAuthorizationContinuous = (authorization, kind) => {
3902
+ const scope = sharedSessionTopology ? 'shared_session' : 'detached_session';
3903
+ const proof = readExactPaneProofSync(authorization.paneId);
3904
+ if (proof.status !== 'live' || proof.pid !== authorization.pid) {
3905
+ throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_identity_changed:${authorization.paneId}`);
3906
+ }
3907
+ const owner = readPaneTeamOwnerTagResult(authorization.paneId);
3908
+ if (owner.status === 'error') {
3909
+ throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_owner_unavailable:${authorization.paneId}:${owner.error}`);
3910
+ }
3911
+ const currentOwner = owner.status === 'value' ? owner.value : null;
3912
+ if (currentOwner !== authorization.owner) {
3913
+ throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_owner_changed:${authorization.paneId}`);
3914
+ }
3915
+ };
3916
+ const persistedHudPaneId = typeof hudPaneId === 'string' && /^%[0-9]+$/.test(hudPaneId)
3917
+ ? hudPaneId
3918
+ : null;
3919
+ const persistedLeaderPaneId = typeof leaderPaneId === 'string' && /^%[0-9]+$/.test(leaderPaneId)
3920
+ ? leaderPaneId
3921
+ : null;
3922
+ if (persistedHudPaneId && effectiveHudPaneId === persistedHudPaneId && (typeof hudPanePid !== 'number' || !Number.isSafeInteger(hudPanePid) || hudPanePid <= 0)) {
3923
+ const scope = sharedSessionTopology ? 'shared_session' : 'detached_session';
3924
+ throw new Error(`shutdown_${scope}_HUD_pane_pid_missing:${effectiveHudPaneId}`);
3003
3925
  }
3004
- let resizeHookWarning = null;
3005
- if (config.resize_hook_name && config.resize_hook_target) {
3006
- const resizeHookName = config.resize_hook_name;
3007
- const unregistered = unregisterResizeHook(config.resize_hook_target, resizeHookName);
3008
- if (!unregistered && isTmuxAvailable()) {
3009
- const baseSession = sessionName.split(':')[0];
3010
- const sessionStillActive = listTeamSessions().includes(baseSession);
3011
- if (sessionStillActive) {
3012
- resizeHookWarning = `failed to unregister resize hook ${resizeHookName}`;
3926
+ const frozenHudAuthorization = effectiveHudPaneId
3927
+ ? (sharedSessionTopology
3928
+ ? freezeSharedPaneAuthorization(effectiveHudPaneId, 'HUD pane', effectiveHudPaneId === persistedHudPaneId ? hudPanePid : undefined)
3929
+ : (() => {
3930
+ if (!persistedHudPaneId || effectiveHudPaneId !== persistedHudPaneId || !tmuxPaneOwnerId
3931
+ || typeof hudPanePid !== 'number' || !Number.isSafeInteger(hudPanePid) || hudPanePid <= 0) {
3932
+ throw new Error(`shutdown_detached_session_HUD_pane_authorization_unavailable:${effectiveHudPaneId}`);
3933
+ }
3934
+ const proof = readExactPaneProofSync(effectiveHudPaneId);
3935
+ if (proof.status !== 'live' || proof.pid !== hudPanePid) {
3936
+ throw new Error(`shutdown_detached_session_HUD_pane_identity_changed:${effectiveHudPaneId}`);
3937
+ }
3938
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
3939
+ if (owner.status !== 'value' || owner.value !== tmuxPaneOwnerId) {
3940
+ throw new Error(`shutdown_detached_session_HUD_pane_owner_changed:${effectiveHudPaneId}`);
3941
+ }
3942
+ const finalProof = readExactPaneProofSync(proof.paneId);
3943
+ if (finalProof.status !== 'live' || finalProof.pid !== hudPanePid) {
3944
+ throw new Error(`shutdown_detached_session_HUD_pane_identity_changed:${effectiveHudPaneId}`);
3945
+ }
3946
+ return { paneId: finalProof.paneId, pid: hudPanePid, owner: tmuxPaneOwnerId };
3947
+ })())
3948
+ : null;
3949
+ if (persistedLeaderPaneId && trustedHudRestoreLeaderPaneId === persistedLeaderPaneId && (typeof leaderPanePid !== 'number' || !Number.isSafeInteger(leaderPanePid) || leaderPanePid <= 0)) {
3950
+ throw new Error(`shutdown_shared_session_restore_leader_pane_pid_missing:${trustedHudRestoreLeaderPaneId}`);
3951
+ }
3952
+ const frozenRestoreLeaderAuthorization = sharedSessionTopology && trustedHudRestoreLeaderPaneId
3953
+ ? freezeSharedPaneAuthorization(trustedHudRestoreLeaderPaneId, 'restore leader pane', trustedHudRestoreLeaderPaneId === persistedLeaderPaneId ? leaderPanePid : undefined)
3954
+ : null;
3955
+ const assertCompleteSharedWorkerPaneProofs = (allowResolvedGone = false) => {
3956
+ const unavailable = readExactPaneProofsSync(canonicalWorkerPaneIds).flatMap((proof) => {
3957
+ if (proof.status !== 'live') {
3958
+ if (allowResolvedGone && proof.status === 'gone' && prekillResolvedWorkerPaneIds.has(proof.paneId))
3959
+ return [];
3960
+ return proof.status === 'unavailable'
3961
+ ? [proof]
3962
+ : [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_proof_lost_during_process_teardown' }];
3963
+ }
3964
+ const persistedPid = config.workers.find((worker) => worker.pane_id?.trim() === proof.paneId)?.pid;
3965
+ if (canonicalExplicitWorkerPaneIds.has(proof.paneId)
3966
+ && (typeof persistedPid !== 'number' || !Number.isSafeInteger(persistedPid) || persistedPid <= 0)) {
3967
+ return [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_pid_changed', detail: 'canonical persisted pane PID is missing' }];
3968
+ }
3969
+ const expectedPid = typeof persistedPid === 'number' ? persistedPid : expectedSharedWorkerPanePids.get(proof.paneId);
3970
+ if (typeof expectedPid === 'number' && proof.pid !== expectedPid) {
3971
+ return [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_pid_changed', detail: `expected ${expectedPid}, got ${proof.pid}` }];
3972
+ }
3973
+ expectedSharedWorkerPanePids.set(proof.paneId, proof.pid);
3974
+ return [];
3975
+ });
3976
+ assertPaneTeardownProofsAvailable('shutdown', unavailable);
3977
+ };
3978
+ const frozenSharedWorkerOwnerIds = new Map();
3979
+ if (sharedSessionTopology) {
3980
+ assertCompleteSharedWorkerPaneProofs();
3981
+ for (const paneId of canonicalWorkerPaneIds) {
3982
+ const owner = readPaneTeamOwnerTagResult(paneId);
3983
+ if (owner.status === 'error') {
3984
+ throw new Error(`shutdown_shared_session_worker_owner_unavailable:${paneId}:${owner.error}`);
3985
+ }
3986
+ if (owner.status !== 'value' || !tmuxPaneOwnerId || owner.value !== tmuxPaneOwnerId) {
3987
+ throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
3013
3988
  }
3989
+ frozenSharedWorkerOwnerIds.set(paneId, owner.value);
3014
3990
  }
3015
3991
  }
3016
- config.resize_hook_name = null;
3017
- config.resize_hook_target = null;
3018
- await saveTeamConfig(config, cwd);
3019
- if (resizeHookWarning) {
3020
- console.warn(`[team shutdown] ${sanitized}: ${resizeHookWarning}; continuing teardown`);
3992
+ const authorizeFrozenSharedWorkerProcessSignal = (paneId, panePid) => {
3993
+ const expectedPid = expectedSharedWorkerPanePids.get(paneId)
3994
+ ?? config.workers.find((worker) => worker.pane_id === paneId)?.pid;
3995
+ if (expectedPid !== panePid)
3996
+ return false;
3997
+ if (sharedSessionTopology && frozenSharedWorkerOwnerIds.has(paneId)) {
3998
+ const expectedOwner = frozenSharedWorkerOwnerIds.get(paneId);
3999
+ const owner = readPaneTeamOwnerTagResult(paneId);
4000
+ return owner.status === 'value' && owner.value === expectedOwner && expectedOwner === tmuxPaneOwnerId;
4001
+ }
4002
+ const owner = readPaneTeamOwnerTagResult(paneId);
4003
+ return owner.status === 'value' && owner.value === tmuxPaneOwnerId;
4004
+ };
4005
+ if (shouldPrekillInteractiveShutdownProcessTrees(sessionName)) {
4006
+ const paneProcessProofUnavailable = [];
4007
+ for (const paneId of shutdownPaneIds) {
4008
+ const teardown = await terminateExactPaneProcessTree(paneId, config.workers.find((worker) => worker.pane_id === paneId)?.pid ?? expectedSharedWorkerPanePids.get(paneId), undefined, undefined, authorizeFrozenSharedWorkerProcessSignal);
4009
+ if (teardown.proofUnavailable) {
4010
+ await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd, config, paneId, teardown });
4011
+ paneProcessProofUnavailable.push(teardown.proofUnavailable);
4012
+ break;
4013
+ }
4014
+ if (!teardown.terminated && teardown.stopped) {
4015
+ await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd, config, paneId, teardown });
4016
+ paneProcessProofUnavailable.push({
4017
+ status: 'unavailable',
4018
+ paneId,
4019
+ reason: 'pane_proof_lost_during_process_teardown',
4020
+ detail: 'exact pane authority was lost before the tracked process tree was resolved',
4021
+ });
4022
+ break;
4023
+ }
4024
+ if (teardown.terminated && readExactPaneProofSync(paneId).status === 'gone') {
4025
+ prekillResolvedWorkerPaneIds.add(paneId);
4026
+ }
4027
+ }
4028
+ if (paneProcessProofUnavailable.length > 0) {
4029
+ assertPaneTeardownProofsAvailable('shutdown', paneProcessProofUnavailable);
4030
+ }
4031
+ }
4032
+ // Revalidate ownership of the frozen discovered members, then re-prove the
4033
+ // frozen complete worker set in one snapshot immediately before the first
4034
+ // HUD topology effect. This never admits a newly discovered pane.
4035
+ if (sharedSessionTopology) {
4036
+ const reauthorizedDiscoveredWorkerPaneIds = collectAuthorizedSharedSessionWorkerPaneIds(authorizedDiscoveredWorkerPaneIds, tmuxPaneOwnerId, canonicalExplicitWorkerPaneIds, initiallyTaggedWorkerPaneIds);
4037
+ if (reauthorizedDiscoveredWorkerPaneIds.length !== authorizedDiscoveredWorkerPaneIds.length
4038
+ || reauthorizedDiscoveredWorkerPaneIds.some((paneId) => !authorizedDiscoveredWorkerPaneIds.includes(paneId))) {
4039
+ throw new Error('shutdown_shared_session_worker_owner_changed');
4040
+ }
4041
+ assertCompleteSharedWorkerPaneProofs(true);
3021
4042
  }
3022
4043
  let restoredHudPaneId = null;
3023
4044
  if (effectiveHudPaneId) {
3024
- await killWorkerByPaneIdAsync(effectiveHudPaneId, effectiveLeaderPaneId ?? undefined);
4045
+ // Confirm restoration authority before removing the shared HUD, then
4046
+ // check it again immediately before the split below.
4047
+ if (sessionName.includes(':') && frozenRestoreLeaderAuthorization) {
4048
+ assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane');
4049
+ }
4050
+ if (frozenHudAuthorization) {
4051
+ assertSharedPaneAuthorizationContinuous(frozenHudAuthorization, 'HUD pane');
4052
+ }
4053
+ const hudTeardown = await teardownWorkerPanes([effectiveHudPaneId], {
4054
+ leaderPaneId: effectiveLeaderPaneId,
4055
+ expectedPanePids: frozenHudAuthorization
4056
+ ? { [frozenHudAuthorization.paneId]: frozenHudAuthorization.pid }
4057
+ : undefined,
4058
+ authorizePaneKill: frozenHudAuthorization
4059
+ ? () => {
4060
+ assertSharedPaneAuthorizationContinuous(frozenHudAuthorization, 'HUD pane');
4061
+ return true;
4062
+ }
4063
+ : undefined,
4064
+ });
4065
+ assertPaneTeardownProofsAvailable('shutdown', hudTeardown.proofUnavailable);
4066
+ if (hudTeardown.kill.failed > 0) {
4067
+ throw new Error(`shutdown_pane_teardown_failed:${hudTeardown.kill.failedPaneIds.join(',')}`);
4068
+ }
3025
4069
  if (sessionName.includes(':')) {
4070
+ if (frozenRestoreLeaderAuthorization) {
4071
+ assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane');
4072
+ }
3026
4073
  restoredHudPaneId = restoreStandaloneHudPane(trustedHudRestoreLeaderPaneId, cwd, {
3027
4074
  sessionId: leaderSessionId,
4075
+ expectedLeaderPanePid: frozenRestoreLeaderAuthorization?.pid,
4076
+ expectedLeaderPaneOwnerId: tmuxPaneOwnerId,
4077
+ stateRoot: join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized),
4078
+ assertLeaderPaneAuthorization: frozenRestoreLeaderAuthorization
4079
+ ? () => assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane')
4080
+ : undefined,
3028
4081
  });
3029
4082
  if (!restoredHudPaneId) {
3030
4083
  const reason = trustedHudRestoreLeaderPaneId
@@ -3033,29 +4086,65 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
3033
4086
  console.warn(`[team shutdown] ${sanitized}: ${reason}`);
3034
4087
  }
3035
4088
  }
4089
+ if (restoredHudPaneId) {
4090
+ const restoredHudProof = readExactPaneProofSync(restoredHudPaneId);
4091
+ if (restoredHudProof.status !== 'live') {
4092
+ throw new Error(`shutdown_restored_hud_pane_proof_unavailable:${restoredHudPaneId}`);
4093
+ }
4094
+ config = await mutateShutdownConfig(config, cwd, (current) => {
4095
+ current.hud_pane_id = restoredHudPaneId;
4096
+ current.hud_pane_pid = restoredHudProof.pid;
4097
+ });
4098
+ finalizeRestoredHudCleanupDebtSync(cwd, restoredHudPaneId, restoredHudProof.pid, join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized), true);
4099
+ }
3036
4100
  }
3037
- shutdownPaneIds = collectShutdownPaneIds({
3038
- config,
3039
- candidatePaneIds: sessionName.includes(':')
3040
- ? filterSharedSessionShutdownWorkerPaneIdsByOwner(resolveSharedSessionShutdownTopology(sessionName, effectiveLeaderPaneId, sanitized).teamWorkerPaneIds, tmuxPaneOwnerId, legacyPersistedWorkerPaneIds, (paneId, error) => warnOwnerReadError('worker pane', paneId, error))
3041
- : listPaneIds(sessionName),
3042
- includePersistedWorkerPaneIds: !sessionName.includes(':'),
3043
- restoredStandaloneHudPaneId: restoredHudPaneId,
3044
- leaderPaneId: effectiveLeaderPaneId,
3045
- hudPaneId: effectiveHudPaneId,
3046
- });
3047
- await teardownWorkerPanes(shutdownPaneIds, {
4101
+ let resizeHookWarning = null;
4102
+ if (config.resize_hook_name && config.resize_hook_target) {
4103
+ const resizeHookName = config.resize_hook_name;
4104
+ const unregistered = unregisterResizeHook(config.resize_hook_target, resizeHookName);
4105
+ if (!unregistered && isTmuxAvailable()) {
4106
+ const baseSession = sessionName.split(':')[0];
4107
+ const teamSessions = listTeamSessions();
4108
+ if (teamSessions === null || teamSessions.includes(baseSession)) {
4109
+ resizeHookWarning = `failed to unregister resize hook ${resizeHookName}`;
4110
+ }
4111
+ }
4112
+ }
4113
+ if (resizeHookWarning) {
4114
+ console.warn(`[team shutdown] ${sanitized}: ${resizeHookWarning}; continuing teardown`);
4115
+ }
4116
+ const workerTeardown = await teardownWorkerPanes(shutdownPaneIds.filter((paneId) => !prekillResolvedWorkerPaneIds.has(paneId)), {
3048
4117
  leaderPaneId: effectiveLeaderPaneId,
3049
4118
  hudPaneId: restoredHudPaneId ?? effectiveHudPaneId,
4119
+ expectedPanePids: {
4120
+ ...Object.fromEntries(expectedSharedWorkerPanePids),
4121
+ ...Object.fromEntries(config.workers
4122
+ .filter((worker) => typeof worker.pane_id === 'string' && typeof worker.pid === 'number')
4123
+ .map((worker) => [worker.pane_id, worker.pid])),
4124
+ },
4125
+ authorizePaneKill: sharedSessionTopology
4126
+ ? (paneId) => {
4127
+ const expectedOwner = frozenSharedWorkerOwnerIds.get(paneId);
4128
+ if (expectedOwner === undefined)
4129
+ return false;
4130
+ const currentOwner = readPaneTeamOwnerTagResult(paneId);
4131
+ return currentOwner.status === 'value'
4132
+ && currentOwner.value === expectedOwner
4133
+ && expectedOwner === tmuxPaneOwnerId;
4134
+ }
4135
+ : undefined,
4136
+ });
4137
+ assertPaneTeardownProofsAvailable('shutdown', workerTeardown.proofUnavailable);
4138
+ if (workerTeardown.kill.failed > 0) {
4139
+ throw new Error(`shutdown_pane_teardown_failed:${workerTeardown.kill.failedPaneIds.join(',')}`);
4140
+ }
4141
+ config = await mutateShutdownConfig(config, cwd, (current) => {
4142
+ current.resize_hook_name = null;
4143
+ current.resize_hook_target = null;
3050
4144
  });
3051
- // 4. Destroy tmux session
4145
+ // 4. Destroy a detached session only after an authoritative leader proof.
3052
4146
  if (!sessionName.includes(':')) {
3053
- try {
3054
- destroyTeamSession(sessionName);
3055
- }
3056
- catch (err) {
3057
- process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
3058
- }
4147
+ await destroyConfiguredDetachedTeamSession(sanitized, cwd, config);
3059
4148
  }
3060
4149
  }
3061
4150
  else {
@@ -3198,7 +4287,7 @@ export async function resumeTeam(teamName, cwd) {
3198
4287
  .filter((worker) => {
3199
4288
  if (!Number.isFinite(worker.pid) || (worker.pid ?? 0) <= 0)
3200
4289
  return false;
3201
- return isPidAlive(worker.pid);
4290
+ return probePidLiveness(worker.pid) !== 'gone';
3202
4291
  })
3203
4292
  .filter((worker) => !getPromptWorkerHandle(sanitized, worker.name));
3204
4293
  if (missingHandles.length > 0) {
@@ -3230,7 +4319,10 @@ async function findActiveTeams(cwd, leaderSessionId) {
3230
4319
  const root = teamRuntimeTeamsRoot(cwd);
3231
4320
  if (!existsSync(root))
3232
4321
  return [];
3233
- const sessions = new Set(listTeamSessions());
4322
+ const listedSessions = listTeamSessions();
4323
+ if (listedSessions === null)
4324
+ throw new Error('tmux_session_query_unavailable');
4325
+ const sessions = new Set(listedSessions);
3234
4326
  const entries = await readdir(root, { withFileTypes: true });
3235
4327
  const active = [];
3236
4328
  for (const e of entries) {
@@ -3266,7 +4358,10 @@ async function detectAndCleanStaleTeam(teamName, leaderCwd, workerCount, confirm
3266
4358
  const stateDir = teamRuntimeTeamRoot(teamName, leaderCwd);
3267
4359
  if (!existsSync(stateDir))
3268
4360
  return;
3269
- const sessions = new Set(listTeamSessions());
4361
+ const listedSessions = listTeamSessions();
4362
+ if (listedSessions === null)
4363
+ return;
4364
+ const sessions = new Set(listedSessions);
3270
4365
  if (sessions.has(`omx-team-${teamName}`))
3271
4366
  return;
3272
4367
  const repoRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], {
@@ -3424,7 +4519,7 @@ async function notifyWorkerOutcome(config, workerIndex, message, workerPaneId) {
3424
4519
  return { ok: false, transport: 'tmux_send_keys', reason: 'tmux_unavailable' };
3425
4520
  }
3426
4521
  try {
3427
- await sendToWorker(config.tmux_session, workerIndex, message, workerPaneId, worker.worker_cli);
4522
+ await sendToWorker(config.tmux_session, workerIndex, message, workerPaneId, worker.worker_cli, worker.pid ?? undefined, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
3428
4523
  return { ok: true, transport: 'tmux_send_keys', reason: 'tmux_send_keys_sent' };
3429
4524
  }
3430
4525
  catch (error) {