oh-my-codex 0.20.3 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1126) hide show
  1. package/Cargo.lock +35 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +22 -19
  4. package/crates/omx-runtime/Cargo.toml +3 -0
  5. package/crates/omx-runtime/src/main.rs +390 -0
  6. package/crates/omx-runtime/tests/execution.rs +264 -0
  7. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  8. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  9. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  10. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  11. package/dist/adapt/contracts.d.ts +1 -1
  12. package/dist/adapt/contracts.d.ts.map +1 -1
  13. package/dist/adapt/contracts.js +1 -1
  14. package/dist/adapt/contracts.js.map +1 -1
  15. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  16. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  17. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  18. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  19. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  20. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  21. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  23. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  24. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  25. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  27. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  28. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  29. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  31. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  32. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  33. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  35. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  36. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  37. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  39. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  40. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  41. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  43. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  44. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  45. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  47. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  48. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  49. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  51. package/dist/adapt/herdr/authority.d.ts +34 -0
  52. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  53. package/dist/adapt/herdr/authority.js +71 -0
  54. package/dist/adapt/herdr/authority.js.map +1 -0
  55. package/dist/adapt/herdr/bridge.d.ts +84 -0
  56. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  57. package/dist/adapt/herdr/bridge.js +222 -0
  58. package/dist/adapt/herdr/bridge.js.map +1 -0
  59. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  60. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  61. package/dist/adapt/herdr/sanitize.js +70 -0
  62. package/dist/adapt/herdr/sanitize.js.map +1 -0
  63. package/dist/adapt/herdr/semantic.d.ts +67 -0
  64. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  65. package/dist/adapt/herdr/semantic.js +170 -0
  66. package/dist/adapt/herdr/semantic.js.map +1 -0
  67. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  68. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  69. package/dist/adapt/herdr/seq-store.js +126 -0
  70. package/dist/adapt/herdr/seq-store.js.map +1 -0
  71. package/dist/adapt/herdr/team-map.d.ts +30 -0
  72. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  73. package/dist/adapt/herdr/team-map.js +37 -0
  74. package/dist/adapt/herdr/team-map.js.map +1 -0
  75. package/dist/adapt/herdr/transport.d.ts +115 -0
  76. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  77. package/dist/adapt/herdr/transport.js +260 -0
  78. package/dist/adapt/herdr/transport.js.map +1 -0
  79. package/dist/adapt/herdr/wiring.d.ts +24 -0
  80. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  81. package/dist/adapt/herdr/wiring.js +62 -0
  82. package/dist/adapt/herdr/wiring.js.map +1 -0
  83. package/dist/adapt/herdr.d.ts +19 -0
  84. package/dist/adapt/herdr.d.ts.map +1 -0
  85. package/dist/adapt/herdr.js +115 -0
  86. package/dist/adapt/herdr.js.map +1 -0
  87. package/dist/adapt/index.d.ts +4 -0
  88. package/dist/adapt/index.d.ts.map +1 -1
  89. package/dist/adapt/index.js +48 -0
  90. package/dist/adapt/index.js.map +1 -1
  91. package/dist/adapt/registry.d.ts.map +1 -1
  92. package/dist/adapt/registry.js +11 -0
  93. package/dist/adapt/registry.js.map +1 -1
  94. package/dist/agents/__tests__/definitions.test.js +0 -27
  95. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  96. package/dist/agents/__tests__/native-config.test.js +3 -8
  97. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  98. package/dist/agents/definitions.d.ts.map +1 -1
  99. package/dist/agents/definitions.js +0 -41
  100. package/dist/agents/definitions.js.map +1 -1
  101. package/dist/agents/policy.d.ts.map +1 -1
  102. package/dist/agents/policy.js +0 -1
  103. package/dist/agents/policy.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +16 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +22 -7
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts +2 -0
  115. package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts.map +1 -0
  116. package/dist/autopilot/__tests__/completion-gate-advisory.test.js +383 -0
  117. package/dist/autopilot/__tests__/completion-gate-advisory.test.js.map +1 -0
  118. package/dist/autopilot/completion-gate.d.ts +6 -1
  119. package/dist/autopilot/completion-gate.d.ts.map +1 -1
  120. package/dist/autopilot/completion-gate.js +247 -7
  121. package/dist/autopilot/completion-gate.js.map +1 -1
  122. package/dist/catalog/__tests__/generator.test.js +15 -10
  123. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  124. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +13 -7
  125. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  126. package/dist/catalog/__tests__/schema.test.js +3 -5
  127. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  128. package/dist/catalog/schema.js +1 -1
  129. package/dist/catalog/schema.js.map +1 -1
  130. package/dist/cli/__tests__/agents-init.test.js +4 -23
  131. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  132. package/dist/cli/__tests__/api.test.js +61 -24
  133. package/dist/cli/__tests__/api.test.js.map +1 -1
  134. package/dist/cli/__tests__/auth.test.js +3 -1
  135. package/dist/cli/__tests__/auth.test.js.map +1 -1
  136. package/dist/cli/__tests__/autopilot.test.d.ts +2 -0
  137. package/dist/cli/__tests__/autopilot.test.d.ts.map +1 -0
  138. package/dist/cli/__tests__/autopilot.test.js +143 -0
  139. package/dist/cli/__tests__/autopilot.test.js.map +1 -0
  140. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  141. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  142. package/dist/cli/__tests__/codex-plugin-layout.test.js +76 -4
  143. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  144. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  145. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  146. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  147. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  148. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  149. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  150. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  151. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  152. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  153. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  154. package/dist/cli/__tests__/doctor-state-root-binding.test.js +206 -0
  155. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  156. package/dist/cli/__tests__/doctor-team.test.js +85 -0
  157. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  158. package/dist/cli/__tests__/doctor-warning-copy.test.js +38 -6
  159. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  160. package/dist/cli/__tests__/exec.test.js +75 -0
  161. package/dist/cli/__tests__/exec.test.js.map +1 -1
  162. package/dist/cli/__tests__/index.test.js +621 -460
  163. package/dist/cli/__tests__/index.test.js.map +1 -1
  164. package/dist/cli/__tests__/launch-fallback.test.js +1125 -143
  165. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  166. package/dist/cli/__tests__/mcp-parity.test.js +4 -3
  167. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  168. package/dist/cli/__tests__/native-assets.test.js +398 -16
  169. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  170. package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
  171. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  172. package/dist/cli/__tests__/package-bin-contract.test.js +5 -8
  173. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  174. package/dist/cli/__tests__/ralph-deslop-contract.test.js +4 -18
  175. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  176. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +4 -30
  177. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  178. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +4 -15
  179. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  180. package/dist/cli/__tests__/ralph.test.js +2 -2
  181. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  182. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
  183. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  184. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  185. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  186. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  187. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  188. package/dist/cli/__tests__/ralplan.test.js +131 -14
  189. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  190. package/dist/cli/__tests__/session-scoped-runtime.test.js +1153 -43
  191. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  192. package/dist/cli/__tests__/session-search-help.test.js +65 -3
  193. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  194. package/dist/cli/__tests__/setup-agents-overwrite.test.js +57 -55
  195. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  196. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +34 -0
  197. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  198. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +1 -1
  199. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -1
  200. package/dist/cli/__tests__/setup-install-mode.test.js +258 -26
  201. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  202. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +1 -1
  203. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  204. package/dist/cli/__tests__/setup-refresh.test.js +3 -6
  205. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  206. package/dist/cli/__tests__/setup-skills-overwrite.test.js +180 -21
  207. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  208. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  209. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  210. package/dist/cli/__tests__/team.test.js +45 -1
  211. package/dist/cli/__tests__/team.test.js.map +1 -1
  212. package/dist/cli/__tests__/ultragoal.test.js +161 -2
  213. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  214. package/dist/cli/__tests__/uninstall.test.js +73 -28
  215. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  216. package/dist/cli/__tests__/update.test.js +749 -177
  217. package/dist/cli/__tests__/update.test.js.map +1 -1
  218. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
  219. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  220. package/dist/cli/api.d.ts.map +1 -1
  221. package/dist/cli/api.js +9 -10
  222. package/dist/cli/api.js.map +1 -1
  223. package/dist/cli/autopilot.d.ts +8 -0
  224. package/dist/cli/autopilot.d.ts.map +1 -0
  225. package/dist/cli/autopilot.js +160 -0
  226. package/dist/cli/autopilot.js.map +1 -0
  227. package/dist/cli/codex-feature-probe.d.ts +16 -0
  228. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  229. package/dist/cli/codex-feature-probe.js +44 -0
  230. package/dist/cli/codex-feature-probe.js.map +1 -1
  231. package/dist/cli/codex-home.d.ts.map +1 -1
  232. package/dist/cli/codex-home.js +7 -4
  233. package/dist/cli/codex-home.js.map +1 -1
  234. package/dist/cli/doctor.d.ts +29 -0
  235. package/dist/cli/doctor.d.ts.map +1 -1
  236. package/dist/cli/doctor.js +433 -19
  237. package/dist/cli/doctor.js.map +1 -1
  238. package/dist/cli/index.d.ts +231 -12
  239. package/dist/cli/index.d.ts.map +1 -1
  240. package/dist/cli/index.js +2354 -724
  241. package/dist/cli/index.js.map +1 -1
  242. package/dist/cli/mcp-parity.d.ts.map +1 -1
  243. package/dist/cli/mcp-parity.js +7 -3
  244. package/dist/cli/mcp-parity.js.map +1 -1
  245. package/dist/cli/native-assets.d.ts +26 -1
  246. package/dist/cli/native-assets.d.ts.map +1 -1
  247. package/dist/cli/native-assets.js +448 -51
  248. package/dist/cli/native-assets.js.map +1 -1
  249. package/dist/cli/native-hook-claim-journal.d.ts +8 -4
  250. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  251. package/dist/cli/native-hook-claim-journal.js +30 -8
  252. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  253. package/dist/cli/package-manager-ownership.d.ts +69 -0
  254. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  255. package/dist/cli/package-manager-ownership.js +338 -0
  256. package/dist/cli/package-manager-ownership.js.map +1 -0
  257. package/dist/cli/plugin-marketplace.d.ts +1 -0
  258. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  259. package/dist/cli/plugin-marketplace.js +123 -55
  260. package/dist/cli/plugin-marketplace.js.map +1 -1
  261. package/dist/cli/ralph.js +2 -2
  262. package/dist/cli/ralph.js.map +1 -1
  263. package/dist/cli/ralplan.d.ts +6 -2
  264. package/dist/cli/ralplan.d.ts.map +1 -1
  265. package/dist/cli/ralplan.js +134 -34
  266. package/dist/cli/ralplan.js.map +1 -1
  267. package/dist/cli/session-search.d.ts.map +1 -1
  268. package/dist/cli/session-search.js +38 -1
  269. package/dist/cli/session-search.js.map +1 -1
  270. package/dist/cli/setup-preferences.d.ts +15 -0
  271. package/dist/cli/setup-preferences.d.ts.map +1 -1
  272. package/dist/cli/setup-preferences.js +27 -1
  273. package/dist/cli/setup-preferences.js.map +1 -1
  274. package/dist/cli/setup.d.ts +6 -0
  275. package/dist/cli/setup.d.ts.map +1 -1
  276. package/dist/cli/setup.js +425 -215
  277. package/dist/cli/setup.js.map +1 -1
  278. package/dist/cli/sparkshell.d.ts.map +1 -1
  279. package/dist/cli/sparkshell.js +52 -9
  280. package/dist/cli/sparkshell.js.map +1 -1
  281. package/dist/cli/ultragoal.d.ts +1 -1
  282. package/dist/cli/ultragoal.d.ts.map +1 -1
  283. package/dist/cli/ultragoal.js +10 -7
  284. package/dist/cli/ultragoal.js.map +1 -1
  285. package/dist/cli/uninstall.d.ts.map +1 -1
  286. package/dist/cli/uninstall.js +27 -28
  287. package/dist/cli/uninstall.js.map +1 -1
  288. package/dist/cli/update-worker.d.ts +2 -0
  289. package/dist/cli/update-worker.d.ts.map +1 -0
  290. package/dist/cli/update-worker.js +142 -0
  291. package/dist/cli/update-worker.js.map +1 -0
  292. package/dist/cli/update.d.ts +16 -23
  293. package/dist/cli/update.d.ts.map +1 -1
  294. package/dist/cli/update.js +212 -241
  295. package/dist/cli/update.js.map +1 -1
  296. package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts +2 -0
  297. package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts.map +1 -0
  298. package/dist/compat/__tests__/upgrade-from-0-20.test.js +109 -0
  299. package/dist/compat/__tests__/upgrade-from-0-20.test.js.map +1 -0
  300. package/dist/compat/upgrade-from-0-20.d.ts +64 -0
  301. package/dist/compat/upgrade-from-0-20.d.ts.map +1 -0
  302. package/dist/compat/upgrade-from-0-20.js +276 -0
  303. package/dist/compat/upgrade-from-0-20.js.map +1 -0
  304. package/dist/config/__tests__/generator-idempotent.test.js +173 -29
  305. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  306. package/dist/config/__tests__/generator-notify.test.js +3 -1
  307. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  308. package/dist/config/generator.d.ts +25 -3
  309. package/dist/config/generator.d.ts.map +1 -1
  310. package/dist/config/generator.js +113 -35
  311. package/dist/config/generator.js.map +1 -1
  312. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  313. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  314. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  315. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  316. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  317. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  318. package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
  319. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  320. package/dist/hooks/__tests__/analyze-skill-contract.test.js +24 -31
  321. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  322. package/dist/hooks/__tests__/anti-slop-workflow.test.js +66 -135
  323. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  324. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +47 -106
  325. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  326. package/dist/hooks/__tests__/code-review-skill-contract.test.js +16 -18
  327. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  328. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +2 -14
  329. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +1 -1
  330. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +56 -234
  331. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  332. package/dist/hooks/__tests__/deep-interview-contract.test.js +1 -3
  333. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  334. package/dist/hooks/__tests__/design-skill.test.js +18 -16
  335. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  336. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +24 -20
  337. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  338. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  339. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  340. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +164 -0
  341. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  342. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts +2 -0
  343. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts.map +1 -0
  344. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js +169 -0
  345. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js.map +1 -0
  346. package/dist/hooks/__tests__/keyword-detector.test.js +226 -858
  347. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  348. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +176 -14
  349. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  350. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  351. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  352. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +33 -21
  353. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  354. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  355. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  356. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  357. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  358. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +14 -904
  359. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  360. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  361. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  362. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +5 -5
  363. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  364. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +7 -7
  365. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  366. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +11 -8
  367. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  368. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +7 -6
  369. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  370. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  371. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  372. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +1 -0
  373. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  374. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +19 -21
  375. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  376. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +23 -3
  377. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  378. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +23 -28
  379. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  380. package/dist/hooks/__tests__/research-workflow-boundaries.test.js +9 -8
  381. package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -1
  382. package/dist/hooks/__tests__/session.test.js +3656 -656
  383. package/dist/hooks/__tests__/session.test.js.map +1 -1
  384. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +13 -22
  385. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  386. package/dist/hooks/__tests__/skill-guidance-contract.test.js +3 -13
  387. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  388. package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts +2 -0
  389. package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts.map +1 -0
  390. package/dist/hooks/__tests__/sunset-routing-contract.test.js +172 -0
  391. package/dist/hooks/__tests__/sunset-routing-contract.test.js.map +1 -0
  392. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +2 -2
  393. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  394. package/dist/hooks/__tests__/tmux-hook-engine.test.js +18 -3
  395. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  396. package/dist/hooks/__tests__/visual-ralph-skill.test.js +20 -17
  397. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  398. package/dist/hooks/__tests__/visual-verdict-loop.test.js +17 -18
  399. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  400. package/dist/hooks/agents-overlay.js +1 -1
  401. package/dist/hooks/agents-overlay.js.map +1 -1
  402. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  403. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  404. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  405. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  406. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  407. package/dist/hooks/extensibility/dispatcher.js +20 -3
  408. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  409. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  410. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  411. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  412. package/dist/hooks/extensibility/runtime.js.map +1 -1
  413. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  414. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  415. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  416. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  417. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  418. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  419. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  420. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  421. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  422. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  423. package/dist/hooks/extensibility/sdk.js +1 -1
  424. package/dist/hooks/extensibility/sdk.js.map +1 -1
  425. package/dist/hooks/extensibility/types.d.ts +7 -0
  426. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  427. package/dist/hooks/keyword-detector.d.ts +6 -47
  428. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  429. package/dist/hooks/keyword-detector.js +156 -268
  430. package/dist/hooks/keyword-detector.js.map +1 -1
  431. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  432. package/dist/hooks/keyword-registry.js +14 -26
  433. package/dist/hooks/keyword-registry.js.map +1 -1
  434. package/dist/hooks/native/capability-warnings.d.ts +16 -0
  435. package/dist/hooks/native/capability-warnings.d.ts.map +1 -0
  436. package/dist/hooks/native/capability-warnings.js +40 -0
  437. package/dist/hooks/native/capability-warnings.js.map +1 -0
  438. package/dist/hooks/native/index.d.ts +17 -0
  439. package/dist/hooks/native/index.d.ts.map +1 -0
  440. package/dist/hooks/native/index.js +17 -0
  441. package/dist/hooks/native/index.js.map +1 -0
  442. package/dist/hooks/native/pre-tool-use-advisory.d.ts +17 -0
  443. package/dist/hooks/native/pre-tool-use-advisory.d.ts.map +1 -0
  444. package/dist/hooks/native/pre-tool-use-advisory.js +112 -0
  445. package/dist/hooks/native/pre-tool-use-advisory.js.map +1 -0
  446. package/dist/hooks/native/types.d.ts +30 -0
  447. package/dist/hooks/native/types.d.ts.map +1 -0
  448. package/dist/hooks/native/types.js +22 -0
  449. package/dist/hooks/native/types.js.map +1 -0
  450. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  451. package/dist/hooks/prompt-guidance-contract.js +46 -93
  452. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  453. package/dist/hooks/session.d.ts +197 -18
  454. package/dist/hooks/session.d.ts.map +1 -1
  455. package/dist/hooks/session.js +1939 -271
  456. package/dist/hooks/session.js.map +1 -1
  457. package/dist/hooks/sunset-stub.d.ts +17 -0
  458. package/dist/hooks/sunset-stub.d.ts.map +1 -0
  459. package/dist/hooks/sunset-stub.js +125 -0
  460. package/dist/hooks/sunset-stub.js.map +1 -0
  461. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  462. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  463. package/dist/hud/__tests__/index.test.js +26 -16
  464. package/dist/hud/__tests__/index.test.js.map +1 -1
  465. package/dist/hud/__tests__/reconcile.test.js +83 -78
  466. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  467. package/dist/hud/__tests__/state.test.js +36 -31
  468. package/dist/hud/__tests__/state.test.js.map +1 -1
  469. package/dist/hud/__tests__/tmux-split-realtmux.test.d.ts +2 -0
  470. package/dist/hud/__tests__/tmux-split-realtmux.test.d.ts.map +1 -0
  471. package/dist/hud/__tests__/tmux-split-realtmux.test.js +112 -0
  472. package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -0
  473. package/dist/hud/__tests__/tmux.test.js +217 -198
  474. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  475. package/dist/hud/tmux.d.ts +43 -4
  476. package/dist/hud/tmux.d.ts.map +1 -1
  477. package/dist/hud/tmux.js +1025 -139
  478. package/dist/hud/tmux.js.map +1 -1
  479. package/dist/leader/__tests__/contract.test.js +71 -34
  480. package/dist/leader/__tests__/contract.test.js.map +1 -1
  481. package/dist/leader/contract.d.ts +1 -6
  482. package/dist/leader/contract.d.ts.map +1 -1
  483. package/dist/leader/contract.js +25 -24
  484. package/dist/leader/contract.js.map +1 -1
  485. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  486. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  487. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  488. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  489. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  490. package/dist/mcp/__tests__/state-server-ralph-phase.test.js +29 -4
  491. package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +1 -1
  492. package/dist/mcp/__tests__/state-server-schema.test.js +13 -3
  493. package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
  494. package/dist/mcp/__tests__/state-server.test.js +11 -1033
  495. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  496. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  497. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  498. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  499. package/dist/mcp/hermes-bridge.js +2 -2
  500. package/dist/mcp/hermes-bridge.js.map +1 -1
  501. package/dist/mcp/state-paths.d.ts +78 -4
  502. package/dist/mcp/state-paths.d.ts.map +1 -1
  503. package/dist/mcp/state-paths.js +543 -40
  504. package/dist/mcp/state-paths.js.map +1 -1
  505. package/dist/mcp/state-server.d.ts +68 -97
  506. package/dist/mcp/state-server.d.ts.map +1 -1
  507. package/dist/mcp/state-server.js +116 -67
  508. package/dist/mcp/state-server.js.map +1 -1
  509. package/dist/modes/__tests__/base-autopilot-gates.test.js +1 -168
  510. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  511. package/dist/modes/__tests__/base-autoresearch-contract.test.js +1 -85
  512. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  513. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -35
  514. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  515. package/dist/modes/__tests__/base-session-scope.test.js +10 -158
  516. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  517. package/dist/modes/base.d.ts +6 -6
  518. package/dist/modes/base.d.ts.map +1 -1
  519. package/dist/modes/base.js +116 -90
  520. package/dist/modes/base.js.map +1 -1
  521. package/dist/native-assets/archive.d.ts +17 -0
  522. package/dist/native-assets/archive.d.ts.map +1 -0
  523. package/dist/native-assets/archive.js +238 -0
  524. package/dist/native-assets/archive.js.map +1 -0
  525. package/dist/native-assets/policy.d.ts +42 -0
  526. package/dist/native-assets/policy.d.ts.map +1 -0
  527. package/dist/native-assets/policy.js +155 -0
  528. package/dist/native-assets/policy.js.map +1 -0
  529. package/dist/notifications/__tests__/formatter.test.js +8 -0
  530. package/dist/notifications/__tests__/formatter.test.js.map +1 -1
  531. package/dist/notifications/__tests__/template-engine.test.js +8 -0
  532. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  533. package/dist/notifications/formatter.js +1 -1
  534. package/dist/notifications/formatter.js.map +1 -1
  535. package/dist/notifications/template-engine.js +1 -1
  536. package/dist/notifications/template-engine.js.map +1 -1
  537. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +0 -21
  538. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  539. package/dist/question/autopilot-wait.d.ts.map +1 -1
  540. package/dist/question/autopilot-wait.js +2 -2
  541. package/dist/question/autopilot-wait.js.map +1 -1
  542. package/dist/question/deep-interview.d.ts +1 -2
  543. package/dist/question/deep-interview.d.ts.map +1 -1
  544. package/dist/question/deep-interview.js +4 -4
  545. package/dist/question/deep-interview.js.map +1 -1
  546. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
  547. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
  548. package/dist/ralplan/__tests__/runtime.test.js +56 -821
  549. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  550. package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
  551. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
  552. package/dist/ralplan/documented-leader-preflight.js +356 -54
  553. package/dist/ralplan/documented-leader-preflight.js.map +1 -1
  554. package/dist/ralplan/runtime.d.ts +2 -2
  555. package/dist/ralplan/runtime.d.ts.map +1 -1
  556. package/dist/ralplan/runtime.js +145 -163
  557. package/dist/ralplan/runtime.js.map +1 -1
  558. package/dist/runtime/bridge.d.ts.map +1 -1
  559. package/dist/runtime/bridge.js +45 -0
  560. package/dist/runtime/bridge.js.map +1 -1
  561. package/dist/runtime/run-state.d.ts +5 -1
  562. package/dist/runtime/run-state.d.ts.map +1 -1
  563. package/dist/runtime/run-state.js +15 -8
  564. package/dist/runtime/run-state.js.map +1 -1
  565. package/dist/scripts/__tests__/codex-native-hook.test.js +12490 -19414
  566. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  567. package/dist/scripts/__tests__/docs-site-contract.test.js +2 -2
  568. package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -1
  569. package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts +2 -0
  570. package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts.map +1 -0
  571. package/dist/scripts/__tests__/generated-artifact-drift.test.js +84 -0
  572. package/dist/scripts/__tests__/generated-artifact-drift.test.js.map +1 -0
  573. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  574. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  575. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +235 -0
  576. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  577. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  578. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  579. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  580. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  581. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  582. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  583. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +207 -0
  584. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  585. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  586. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  587. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +156 -0
  588. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  589. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  590. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  591. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +191 -0
  592. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  593. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  594. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  595. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +40 -0
  596. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  597. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts +2 -0
  598. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts.map +1 -0
  599. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js +63 -0
  600. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js.map +1 -0
  601. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts +2 -0
  602. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts.map +1 -0
  603. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js +65 -0
  604. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js.map +1 -0
  605. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts +2 -0
  606. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts.map +1 -0
  607. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.js +189 -0
  608. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.js.map +1 -0
  609. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  610. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  611. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  612. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  613. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  614. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  615. package/dist/scripts/__tests__/prompt-inventory.test.js +54 -1
  616. package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -1
  617. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +2 -2
  618. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -1
  619. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -60
  620. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  621. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
  622. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  623. package/dist/scripts/__tests__/smoke-packed-install.test.js +168 -20
  624. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  625. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  626. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  627. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  628. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  629. package/dist/scripts/codex-native-hook.d.ts +51 -2
  630. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  631. package/dist/scripts/codex-native-hook.js +3648 -1332
  632. package/dist/scripts/codex-native-hook.js.map +1 -1
  633. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  634. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  635. package/dist/scripts/codex-native-pre-post.js +33 -22
  636. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  637. package/dist/scripts/generate-native-release-manifest.js +80 -51
  638. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  639. package/dist/scripts/notify-fallback-watcher.js +28 -39
  640. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  641. package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -48
  642. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  643. package/dist/scripts/notify-hook/auto-nudge.js +1 -0
  644. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  645. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  646. package/dist/scripts/notify-hook/state-io.js +5 -0
  647. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  648. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  649. package/dist/scripts/notify-hook/team-dispatch.js +8 -5
  650. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  651. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  652. package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
  653. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  654. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
  655. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  656. package/dist/scripts/notify-hook/team-tmux-guard.js +3 -2
  657. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  658. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  659. package/dist/scripts/notify-hook/team-worker-stop.js +23 -3
  660. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  661. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  662. package/dist/scripts/notify-hook/team-worker.js +45 -5
  663. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  664. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  665. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  666. package/dist/scripts/notify-hook/tmux-injection.js +4 -1
  667. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  668. package/dist/scripts/notify-hook.d.ts.map +1 -1
  669. package/dist/scripts/notify-hook.js +71 -124
  670. package/dist/scripts/notify-hook.js.map +1 -1
  671. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  672. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  673. package/dist/scripts/postinstall-advisory.js +49 -0
  674. package/dist/scripts/postinstall-advisory.js.map +1 -0
  675. package/dist/scripts/postinstall.d.ts +1 -1
  676. package/dist/scripts/postinstall.d.ts.map +1 -1
  677. package/dist/scripts/postinstall.js +1 -1
  678. package/dist/scripts/postinstall.js.map +1 -1
  679. package/dist/scripts/prompt-inventory.d.ts +39 -0
  680. package/dist/scripts/prompt-inventory.d.ts.map +1 -1
  681. package/dist/scripts/prompt-inventory.js +108 -3
  682. package/dist/scripts/prompt-inventory.js.map +1 -1
  683. package/dist/scripts/run-compiled-ci.js +2 -0
  684. package/dist/scripts/run-compiled-ci.js.map +1 -1
  685. package/dist/scripts/run-test-files.js +1 -0
  686. package/dist/scripts/run-test-files.js.map +1 -1
  687. package/dist/scripts/smoke-packed-install.d.ts +32 -1
  688. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  689. package/dist/scripts/smoke-packed-install.js +407 -60
  690. package/dist/scripts/smoke-packed-install.js.map +1 -1
  691. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  692. package/dist/scripts/sync-plugin-mirror.js +16 -1
  693. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  694. package/dist/scripts/sync-prompt-guidance-fragments.js +62 -20
  695. package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
  696. package/dist/scripts/tmux-hook-engine.d.ts +1 -1
  697. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  698. package/dist/scripts/tmux-hook-engine.js +3 -3
  699. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  700. package/dist/scripts/verify-capabilities-lock.d.ts +3 -0
  701. package/dist/scripts/verify-capabilities-lock.d.ts.map +1 -0
  702. package/dist/scripts/verify-capabilities-lock.js +22 -0
  703. package/dist/scripts/verify-capabilities-lock.js.map +1 -0
  704. package/dist/scripts/verify-native-release-assets.js +37 -23
  705. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  706. package/dist/state/__tests__/authority-decreasing.test.d.ts +2 -0
  707. package/dist/state/__tests__/authority-decreasing.test.d.ts.map +1 -0
  708. package/dist/state/__tests__/authority-decreasing.test.js +78 -0
  709. package/dist/state/__tests__/authority-decreasing.test.js.map +1 -0
  710. package/dist/state/__tests__/handoff-carrier.test.d.ts +2 -0
  711. package/dist/state/__tests__/handoff-carrier.test.d.ts.map +1 -0
  712. package/dist/state/__tests__/handoff-carrier.test.js +96 -0
  713. package/dist/state/__tests__/handoff-carrier.test.js.map +1 -0
  714. package/dist/state/__tests__/mode-state-context.test.js +12 -5
  715. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  716. package/dist/state/__tests__/operations.test.js +870 -2517
  717. package/dist/state/__tests__/operations.test.js.map +1 -1
  718. package/dist/state/__tests__/single-writer-invariant.test.d.ts +2 -0
  719. package/dist/state/__tests__/single-writer-invariant.test.d.ts.map +1 -0
  720. package/dist/state/__tests__/single-writer-invariant.test.js +342 -0
  721. package/dist/state/__tests__/single-writer-invariant.test.js.map +1 -0
  722. package/dist/state/__tests__/skill-active.test.js +400 -2
  723. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  724. package/dist/state/__tests__/stale-state-neutralization.test.d.ts +2 -0
  725. package/dist/state/__tests__/stale-state-neutralization.test.d.ts.map +1 -0
  726. package/dist/state/__tests__/stale-state-neutralization.test.js +84 -0
  727. package/dist/state/__tests__/stale-state-neutralization.test.js.map +1 -0
  728. package/dist/state/__tests__/workflow-transition.test.js +20 -33
  729. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  730. package/dist/state/handoff-carrier.d.ts +46 -0
  731. package/dist/state/handoff-carrier.d.ts.map +1 -0
  732. package/dist/state/handoff-carrier.js +85 -0
  733. package/dist/state/handoff-carrier.js.map +1 -0
  734. package/dist/state/mode-state-context.d.ts +3 -0
  735. package/dist/state/mode-state-context.d.ts.map +1 -1
  736. package/dist/state/mode-state-context.js +27 -13
  737. package/dist/state/mode-state-context.js.map +1 -1
  738. package/dist/state/namespace-owners.d.ts +34 -0
  739. package/dist/state/namespace-owners.d.ts.map +1 -0
  740. package/dist/state/namespace-owners.js +127 -0
  741. package/dist/state/namespace-owners.js.map +1 -0
  742. package/dist/state/operations.d.ts +9 -4
  743. package/dist/state/operations.d.ts.map +1 -1
  744. package/dist/state/operations.js +194 -235
  745. package/dist/state/operations.js.map +1 -1
  746. package/dist/state/skill-active.d.ts +18 -1
  747. package/dist/state/skill-active.d.ts.map +1 -1
  748. package/dist/state/skill-active.js +421 -31
  749. package/dist/state/skill-active.js.map +1 -1
  750. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  751. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  752. package/dist/state/workflow-transition-reconcile.js +11 -31
  753. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  754. package/dist/state/workflow-transition.d.ts +1 -27
  755. package/dist/state/workflow-transition.d.ts.map +1 -1
  756. package/dist/state/workflow-transition.js +9 -790
  757. package/dist/state/workflow-transition.js.map +1 -1
  758. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
  759. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  760. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  761. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  762. package/dist/subagents/tracker.d.ts +64 -60
  763. package/dist/subagents/tracker.d.ts.map +1 -1
  764. package/dist/subagents/tracker.js +809 -358
  765. package/dist/subagents/tracker.js.map +1 -1
  766. package/dist/team/__tests__/api-interop.test.js +1 -0
  767. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  768. package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
  769. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  770. package/dist/team/__tests__/mcp-comm.test.js +104 -3
  771. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  772. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  773. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  774. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  775. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  776. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  777. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  778. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  779. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  780. package/dist/team/__tests__/phase-controller.test.js +20 -0
  781. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  782. package/dist/team/__tests__/runtime.test.js +725 -59
  783. package/dist/team/__tests__/runtime.test.js.map +1 -1
  784. package/dist/team/__tests__/scaling.test.js +269 -245
  785. package/dist/team/__tests__/scaling.test.js.map +1 -1
  786. package/dist/team/__tests__/state-root.test.js +117 -2
  787. package/dist/team/__tests__/state-root.test.js.map +1 -1
  788. package/dist/team/__tests__/team-ops-contract.test.js +1 -0
  789. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  790. package/dist/team/__tests__/tmux-session.test.js +1093 -101
  791. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  792. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  793. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  794. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +159 -0
  795. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  796. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  797. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  798. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  799. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  800. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  801. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  802. package/dist/team/__tests__/worker-bootstrap.test.js +11 -13
  803. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  804. package/dist/team/__tests__/worker-provenance.test.d.ts +2 -0
  805. package/dist/team/__tests__/worker-provenance.test.d.ts.map +1 -0
  806. package/dist/team/__tests__/worker-provenance.test.js +251 -0
  807. package/dist/team/__tests__/worker-provenance.test.js.map +1 -0
  808. package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
  809. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  810. package/dist/team/api-interop.d.ts.map +1 -1
  811. package/dist/team/api-interop.js +14 -8
  812. package/dist/team/api-interop.js.map +1 -1
  813. package/dist/team/mcp-comm.d.ts.map +1 -1
  814. package/dist/team/mcp-comm.js +3 -8
  815. package/dist/team/mcp-comm.js.map +1 -1
  816. package/dist/team/notice-ledger.d.ts +54 -0
  817. package/dist/team/notice-ledger.d.ts.map +1 -0
  818. package/dist/team/notice-ledger.js +259 -0
  819. package/dist/team/notice-ledger.js.map +1 -0
  820. package/dist/team/phase-controller.d.ts.map +1 -1
  821. package/dist/team/phase-controller.js +2 -0
  822. package/dist/team/phase-controller.js.map +1 -1
  823. package/dist/team/runtime.d.ts +3 -1
  824. package/dist/team/runtime.d.ts.map +1 -1
  825. package/dist/team/runtime.js +314 -142
  826. package/dist/team/runtime.js.map +1 -1
  827. package/dist/team/scaling.d.ts.map +1 -1
  828. package/dist/team/scaling.js +161 -91
  829. package/dist/team/scaling.js.map +1 -1
  830. package/dist/team/state/monitor.d.ts +10 -0
  831. package/dist/team/state/monitor.d.ts.map +1 -1
  832. package/dist/team/state/monitor.js +13 -0
  833. package/dist/team/state/monitor.js.map +1 -1
  834. package/dist/team/state/types.d.ts +15 -0
  835. package/dist/team/state/types.d.ts.map +1 -1
  836. package/dist/team/state/types.js.map +1 -1
  837. package/dist/team/state-root.d.ts +12 -0
  838. package/dist/team/state-root.d.ts.map +1 -1
  839. package/dist/team/state-root.js +177 -0
  840. package/dist/team/state-root.js.map +1 -1
  841. package/dist/team/state.d.ts +19 -0
  842. package/dist/team/state.d.ts.map +1 -1
  843. package/dist/team/state.js +47 -1
  844. package/dist/team/state.js.map +1 -1
  845. package/dist/team/team-ops.d.ts +1 -0
  846. package/dist/team/team-ops.d.ts.map +1 -1
  847. package/dist/team/team-ops.js +1 -0
  848. package/dist/team/team-ops.js.map +1 -1
  849. package/dist/team/tmux-session.d.ts +42 -4
  850. package/dist/team/tmux-session.d.ts.map +1 -1
  851. package/dist/team/tmux-session.js +572 -196
  852. package/dist/team/tmux-session.js.map +1 -1
  853. package/dist/team/ultragoal-context.d.ts +1 -0
  854. package/dist/team/ultragoal-context.d.ts.map +1 -1
  855. package/dist/team/ultragoal-context.js +2 -0
  856. package/dist/team/ultragoal-context.js.map +1 -1
  857. package/dist/team/worker-provenance.d.ts +35 -0
  858. package/dist/team/worker-provenance.d.ts.map +1 -0
  859. package/dist/team/worker-provenance.js +213 -0
  860. package/dist/team/worker-provenance.js.map +1 -0
  861. package/dist/ultragoal/__tests__/artifacts.test.js +1053 -2
  862. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  863. package/dist/ultragoal/__tests__/docs-contract.test.js +89 -59
  864. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  865. package/dist/ultragoal/artifacts.d.ts +36 -1
  866. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  867. package/dist/ultragoal/artifacts.js +267 -51
  868. package/dist/ultragoal/artifacts.js.map +1 -1
  869. package/dist/url-reader/__tests__/url-reader.test.js +14 -0
  870. package/dist/url-reader/__tests__/url-reader.test.js.map +1 -1
  871. package/dist/url-reader/index.js +5 -3
  872. package/dist/url-reader/index.js.map +1 -1
  873. package/dist/utils/__tests__/file-durability.test.js +58 -1
  874. package/dist/utils/__tests__/file-durability.test.js.map +1 -1
  875. package/dist/utils/__tests__/paths.test.js +21 -1
  876. package/dist/utils/__tests__/paths.test.js.map +1 -1
  877. package/dist/utils/__tests__/version.test.js +17 -0
  878. package/dist/utils/__tests__/version.test.js.map +1 -1
  879. package/dist/utils/file-durability.d.ts +10 -0
  880. package/dist/utils/file-durability.d.ts.map +1 -1
  881. package/dist/utils/file-durability.js +34 -4
  882. package/dist/utils/file-durability.js.map +1 -1
  883. package/dist/utils/paths.d.ts.map +1 -1
  884. package/dist/utils/paths.js +14 -6
  885. package/dist/utils/paths.js.map +1 -1
  886. package/dist/utils/version.d.ts.map +1 -1
  887. package/dist/utils/version.js +2 -1
  888. package/dist/utils/version.js.map +1 -1
  889. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  890. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  891. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  892. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  893. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  894. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  895. package/package.json +17 -6
  896. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  897. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
  898. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +60 -135
  899. package/plugins/oh-my-codex/skills/analyze/SKILL.md +35 -111
  900. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +72 -163
  901. package/plugins/oh-my-codex/skills/cancel/SKILL.md +33 -392
  902. package/plugins/oh-my-codex/skills/code-review/SKILL.md +66 -256
  903. package/plugins/oh-my-codex/skills/configure-notifications/SKILL.md +30 -216
  904. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -10
  905. package/plugins/oh-my-codex/skills/design/SKILL.md +32 -132
  906. package/plugins/oh-my-codex/skills/doctor/SKILL.md +56 -177
  907. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +35 -116
  908. package/plugins/oh-my-codex/skills/plan/SKILL.md +45 -228
  909. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +20 -22
  910. package/plugins/oh-my-codex/skills/skill/SKILL.md +82 -807
  911. package/plugins/oh-my-codex/skills/team/SKILL.md +32 -520
  912. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +47 -150
  913. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +75 -252
  914. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +33 -111
  915. package/plugins/oh-my-codex/skills/worker/SKILL.md +40 -113
  916. package/prompts/analyst.md +27 -107
  917. package/prompts/api-reviewer.md +17 -84
  918. package/prompts/architect.md +31 -70
  919. package/prompts/code-reviewer.md +18 -107
  920. package/prompts/code-simplifier.md +32 -110
  921. package/prompts/critic.md +32 -54
  922. package/prompts/dependency-expert.md +37 -107
  923. package/prompts/designer.md +40 -107
  924. package/prompts/executor.md +26 -75
  925. package/prompts/information-architect.md +51 -200
  926. package/prompts/performance-reviewer.md +17 -81
  927. package/prompts/planner.md +33 -76
  928. package/prompts/product-analyst.md +65 -280
  929. package/prompts/product-manager.md +48 -216
  930. package/prompts/qa-tester.md +33 -104
  931. package/prompts/quality-reviewer.md +17 -93
  932. package/prompts/quality-strategist.md +31 -255
  933. package/prompts/researcher.md +32 -89
  934. package/prompts/security-reviewer.md +12 -98
  935. package/prompts/test-engineer.md +29 -110
  936. package/prompts/ux-researcher.md +46 -300
  937. package/prompts/verifier.md +23 -51
  938. package/skills/ai-slop-cleaner/SKILL.md +60 -135
  939. package/skills/analyze/SKILL.md +35 -111
  940. package/skills/autopilot/SKILL.md +72 -163
  941. package/skills/autoresearch-goal/SKILL.md +6 -31
  942. package/skills/cancel/SKILL.md +33 -392
  943. package/skills/code-review/SKILL.md +66 -256
  944. package/skills/configure-notifications/SKILL.md +30 -216
  945. package/skills/deep-interview/SKILL.md +11 -10
  946. package/skills/design/SKILL.md +32 -132
  947. package/skills/doctor/SKILL.md +56 -177
  948. package/skills/omx-setup/SKILL.md +35 -116
  949. package/skills/pipeline/SKILL.md +6 -89
  950. package/skills/plan/SKILL.md +45 -228
  951. package/skills/ralph/SKILL.md +6 -290
  952. package/skills/ralplan/SKILL.md +20 -22
  953. package/skills/skill/SKILL.md +82 -807
  954. package/skills/team/SKILL.md +32 -520
  955. package/skills/ultragoal/SKILL.md +47 -150
  956. package/skills/ultraqa/SKILL.md +75 -252
  957. package/skills/ultrawork/SKILL.md +6 -182
  958. package/skills/visual-ralph/SKILL.md +33 -111
  959. package/skills/worker/SKILL.md +40 -113
  960. package/src/scripts/__tests__/codex-native-hook.test.ts +18888 -28799
  961. package/src/scripts/__tests__/docs-site-contract.test.ts +2 -2
  962. package/src/scripts/__tests__/generated-artifact-drift.test.ts +97 -0
  963. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +197 -0
  964. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  965. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +186 -0
  966. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +177 -0
  967. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +201 -0
  968. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +43 -0
  969. package/src/scripts/__tests__/issue-3481-conductor-deadlock.test.ts +77 -0
  970. package/src/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.ts +95 -0
  971. package/src/scripts/__tests__/issue-3536-external-team-state-root.test.ts +216 -0
  972. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  973. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  974. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  975. package/src/scripts/__tests__/prompt-inventory.test.ts +62 -1
  976. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +1 -2
  977. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +49 -59
  978. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
  979. package/src/scripts/__tests__/smoke-packed-install.test.ts +223 -23
  980. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  981. package/src/scripts/codex-native-hook.ts +3618 -1383
  982. package/src/scripts/codex-native-pre-post.ts +50 -24
  983. package/src/scripts/generate-native-release-manifest.ts +83 -79
  984. package/src/scripts/notify-fallback-watcher.ts +38 -38
  985. package/src/scripts/notify-hook/auto-nudge.ts +1 -0
  986. package/src/scripts/notify-hook/state-io.ts +11 -0
  987. package/src/scripts/notify-hook/team-dispatch.ts +8 -5
  988. package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
  989. package/src/scripts/notify-hook/team-tmux-guard.ts +3 -1
  990. package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
  991. package/src/scripts/notify-hook/team-worker.ts +43 -5
  992. package/src/scripts/notify-hook/tmux-injection.ts +5 -1
  993. package/src/scripts/notify-hook.ts +76 -124
  994. package/src/scripts/postinstall-advisory.ts +67 -0
  995. package/src/scripts/postinstall.ts +1 -1
  996. package/src/scripts/prompt-inventory.ts +142 -3
  997. package/src/scripts/run-compiled-ci.ts +2 -0
  998. package/src/scripts/run-test-files.ts +1 -0
  999. package/src/scripts/smoke-packed-install.ts +440 -59
  1000. package/src/scripts/sync-plugin-mirror.ts +17 -1
  1001. package/src/scripts/sync-prompt-guidance-fragments.ts +59 -22
  1002. package/src/scripts/tmux-hook-engine.ts +3 -3
  1003. package/src/scripts/verify-capabilities-lock.ts +23 -0
  1004. package/src/scripts/verify-native-release-assets.ts +39 -31
  1005. package/templates/AGENTS.md +45 -9
  1006. package/templates/catalog-manifest.json +6 -146
  1007. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +0 -2
  1008. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +0 -1
  1009. package/dist/autopilot/__tests__/deep-interview-gate.test.js +0 -215
  1010. package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +0 -1
  1011. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +0 -2
  1012. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +0 -1
  1013. package/dist/autopilot/__tests__/ralplan-gate.test.js +0 -1361
  1014. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +0 -1
  1015. package/dist/autopilot/deep-interview-gate.d.ts +0 -18
  1016. package/dist/autopilot/deep-interview-gate.d.ts.map +0 -1
  1017. package/dist/autopilot/deep-interview-gate.js +0 -396
  1018. package/dist/autopilot/deep-interview-gate.js.map +0 -1
  1019. package/dist/autopilot/ralplan-gate.d.ts +0 -18
  1020. package/dist/autopilot/ralplan-gate.d.ts.map +0 -1
  1021. package/dist/autopilot/ralplan-gate.js +0 -148
  1022. package/dist/autopilot/ralplan-gate.js.map +0 -1
  1023. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +0 -2
  1024. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +0 -1
  1025. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +0 -320
  1026. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +0 -1
  1027. package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
  1028. package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
  1029. package/dist/pipeline/__tests__/orchestrator.test.js +0 -770
  1030. package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
  1031. package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
  1032. package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
  1033. package/dist/pipeline/__tests__/stages.test.js +0 -1778
  1034. package/dist/pipeline/__tests__/stages.test.js.map +0 -1
  1035. package/dist/pipeline/index.d.ts +0 -25
  1036. package/dist/pipeline/index.d.ts.map +0 -1
  1037. package/dist/pipeline/index.js +0 -17
  1038. package/dist/pipeline/index.js.map +0 -1
  1039. package/dist/pipeline/orchestrator.d.ts +0 -52
  1040. package/dist/pipeline/orchestrator.d.ts.map +0 -1
  1041. package/dist/pipeline/orchestrator.js +0 -399
  1042. package/dist/pipeline/orchestrator.js.map +0 -1
  1043. package/dist/pipeline/review-verdict.d.ts +0 -3
  1044. package/dist/pipeline/review-verdict.d.ts.map +0 -1
  1045. package/dist/pipeline/review-verdict.js +0 -14
  1046. package/dist/pipeline/review-verdict.js.map +0 -1
  1047. package/dist/pipeline/stages/code-review.d.ts +0 -35
  1048. package/dist/pipeline/stages/code-review.d.ts.map +0 -1
  1049. package/dist/pipeline/stages/code-review.js +0 -55
  1050. package/dist/pipeline/stages/code-review.js.map +0 -1
  1051. package/dist/pipeline/stages/deep-interview.d.ts +0 -15
  1052. package/dist/pipeline/stages/deep-interview.d.ts.map +0 -1
  1053. package/dist/pipeline/stages/deep-interview.js +0 -32
  1054. package/dist/pipeline/stages/deep-interview.js.map +0 -1
  1055. package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
  1056. package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
  1057. package/dist/pipeline/stages/ralph-verify.js +0 -89
  1058. package/dist/pipeline/stages/ralph-verify.js.map +0 -1
  1059. package/dist/pipeline/stages/ralplan.d.ts +0 -27
  1060. package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
  1061. package/dist/pipeline/stages/ralplan.js +0 -152
  1062. package/dist/pipeline/stages/ralplan.js.map +0 -1
  1063. package/dist/pipeline/stages/team-exec.d.ts +0 -52
  1064. package/dist/pipeline/stages/team-exec.d.ts.map +0 -1
  1065. package/dist/pipeline/stages/team-exec.js +0 -266
  1066. package/dist/pipeline/stages/team-exec.js.map +0 -1
  1067. package/dist/pipeline/stages/ultragoal.d.ts +0 -23
  1068. package/dist/pipeline/stages/ultragoal.d.ts.map +0 -1
  1069. package/dist/pipeline/stages/ultragoal.js +0 -53
  1070. package/dist/pipeline/stages/ultragoal.js.map +0 -1
  1071. package/dist/pipeline/stages/ultraqa.d.ts +0 -33
  1072. package/dist/pipeline/stages/ultraqa.d.ts.map +0 -1
  1073. package/dist/pipeline/stages/ultraqa.js +0 -49
  1074. package/dist/pipeline/stages/ultraqa.js.map +0 -1
  1075. package/dist/pipeline/types.d.ts +0 -124
  1076. package/dist/pipeline/types.d.ts.map +0 -1
  1077. package/dist/pipeline/types.js +0 -8
  1078. package/dist/pipeline/types.js.map +0 -1
  1079. package/dist/ralplan/__tests__/consensus-gate.test.d.ts +0 -2
  1080. package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +0 -1
  1081. package/dist/ralplan/__tests__/consensus-gate.test.js +0 -1455
  1082. package/dist/ralplan/__tests__/consensus-gate.test.js.map +0 -1
  1083. package/dist/ralplan/consensus-gate.d.ts +0 -62
  1084. package/dist/ralplan/consensus-gate.d.ts.map +0 -1
  1085. package/dist/ralplan/consensus-gate.js +0 -760
  1086. package/dist/ralplan/consensus-gate.js.map +0 -1
  1087. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -25
  1088. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +0 -1
  1089. package/dist/scripts/notify-hook/ralph-session-resume.js +0 -377
  1090. package/dist/scripts/notify-hook/ralph-session-resume.js.map +0 -1
  1091. package/dist/state/__tests__/planning-gate.test.d.ts +0 -2
  1092. package/dist/state/__tests__/planning-gate.test.d.ts.map +0 -1
  1093. package/dist/state/__tests__/planning-gate.test.js +0 -629
  1094. package/dist/state/__tests__/planning-gate.test.js.map +0 -1
  1095. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +0 -39
  1096. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +0 -97
  1097. package/plugins/oh-my-codex/skills/prometheus-strict/README.md +0 -35
  1098. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +0 -219
  1099. package/plugins/oh-my-codex/skills/ralph/SKILL.md +0 -298
  1100. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +0 -190
  1101. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +0 -62
  1102. package/prompts/prometheus-strict-metis.md +0 -274
  1103. package/prompts/prometheus-strict-momus.md +0 -82
  1104. package/prompts/prometheus-strict-oracle.md +0 -107
  1105. package/prompts/scholastic.md +0 -11
  1106. package/prompts/sisyphus-lite.md +0 -111
  1107. package/skills/ask-claude/SKILL.md +0 -12
  1108. package/skills/ask-gemini/SKILL.md +0 -12
  1109. package/skills/build-fix/SKILL.md +0 -10
  1110. package/skills/deepsearch/SKILL.md +0 -10
  1111. package/skills/ecomode/SKILL.md +0 -114
  1112. package/skills/ecomode/references/agent-tiers.md +0 -61
  1113. package/skills/frontend-ui-ux/SKILL.md +0 -16
  1114. package/skills/help/SKILL.md +0 -10
  1115. package/skills/note/SKILL.md +0 -10
  1116. package/skills/prometheus-strict/README.md +0 -35
  1117. package/skills/prometheus-strict/SKILL.md +0 -219
  1118. package/skills/ralph-init/SKILL.md +0 -10
  1119. package/skills/review/SKILL.md +0 -10
  1120. package/skills/security-review/SKILL.md +0 -10
  1121. package/skills/swarm/SKILL.md +0 -12
  1122. package/skills/tdd/SKILL.md +0 -104
  1123. package/skills/trace/SKILL.md +0 -10
  1124. package/skills/visual-verdict/SKILL.md +0 -10
  1125. package/skills/web-clone/SKILL.md +0 -357
  1126. package/src/scripts/notify-hook/ralph-session-resume.ts +0 -466
@@ -5,14 +5,26 @@
5
5
  * serialized by an adjacent lock directory and become visible only after an
6
6
  * atomic rename of a transaction-owned temporary file.
7
7
  */
8
- import { appendFile, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
9
- import { readFileSync } from 'fs';
8
+ import { execFileSync as nodeExecFileSync } from 'node:child_process';
9
+ import { appendFile, mkdtemp as nodeMkdtemp, link as nodeLink, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, realpath, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
10
+ import { appendFileSync, closeSync, constants as fsConstants, fstatSync, openSync, readFileSync, realpathSync } from 'fs';
11
+ import { tmpdir } from 'node:os';
10
12
  import { createHash, randomUUID } from 'crypto';
11
- import { dirname, join } from 'path';
13
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path';
12
14
  import { omxRoot, omxLogsDir, sameFilePath } from '../utils/paths.js';
15
+ import { resolveRuntimeBinaryPath } from '../runtime/bridge.js';
13
16
  import { getBaseStateDirWithSource, resolveWorkingDirectoryForState, } from '../mcp/state-paths.js';
14
17
  import { emitDegradedDurabilityWarning, recordRegularFileSyncOutcome, syncRegularFile, } from '../utils/file-durability.js';
18
+ export class CommittedLaunchBlockedError extends Error {
19
+ secondaryFailures;
20
+ name = 'CommittedLaunchBlockedError';
21
+ constructor(secondaryFailures) {
22
+ super('Session pointer committed, but lock release left recovery evidence.');
23
+ this.secondaryFailures = secondaryFailures;
24
+ }
25
+ }
15
26
  const SESSION_FILE = 'session.json';
27
+ const SESSION_OWNER_FILE = 'session-owner.json';
16
28
  const HISTORY_FILE = 'session-history.jsonl';
17
29
  const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
18
30
  const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
@@ -20,6 +32,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
20
32
  const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
21
33
  const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
22
34
  const NATIVE_POINTER_TIMEOUT_MS = 2_000;
35
+ function traceSessionFinalizationOperation(operation) {
36
+ if (process.env.OMX_TEST_DETACHED_TRACE !== '1')
37
+ return;
38
+ const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
39
+ if (!tracePath)
40
+ return;
41
+ appendFileSync(tracePath, `[omx-test-session-finalization] ${operation}\n`);
42
+ }
23
43
  /**
24
44
  * Convert arbitrary input into a valid session ID without exposing validator
25
45
  * exceptions to lifecycle or hook inputs.
@@ -31,9 +51,9 @@ export function normalizeSessionId(value) {
31
51
  return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
32
52
  }
33
53
  /** Resolve the one exact pointer path used by an operation. */
34
- export function resolveSessionPointerContext(cwd) {
35
- const normalizedCwd = resolveWorkingDirectoryForState(cwd);
36
- const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd);
54
+ export function resolveSessionPointerContext(cwd, env = process.env) {
55
+ const normalizedCwd = resolveWorkingDirectoryForState(cwd, env);
56
+ const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd, env);
37
57
  const pointerPath = join(baseStateDir, SESSION_FILE);
38
58
  return {
39
59
  cwd: normalizedCwd,
@@ -43,6 +63,26 @@ export function resolveSessionPointerContext(cwd) {
43
63
  lockPath: `${pointerPath}.lock`,
44
64
  };
45
65
  }
66
+ function resolveNativeSessionOwnerContext(cwd, nativeSessionId) {
67
+ const root = resolveSessionPointerContext(cwd);
68
+ const normalized = normalizeSessionId(nativeSessionId);
69
+ if (!normalized) {
70
+ throw resolvedAbort(root, {
71
+ code: 'session_pointer_io_failure',
72
+ operation: 'pointer-classify',
73
+ lockPath: root.lockPath,
74
+ reason: 'A valid native session ID is required for owner evidence.',
75
+ });
76
+ }
77
+ const baseStateDir = join(root.baseStateDir, 'sessions', normalized);
78
+ const sessionPath = join(baseStateDir, SESSION_OWNER_FILE);
79
+ return {
80
+ ...root,
81
+ baseStateDir,
82
+ sessionPath,
83
+ lockPath: `${sessionPath}.lock`,
84
+ };
85
+ }
46
86
  function attemptedStateRootSource() {
47
87
  try {
48
88
  if (process.env.OMX_TEAM_STATE_ROOT?.trim())
@@ -123,6 +163,7 @@ const defaultFsDependencies = {
123
163
  },
124
164
  readdir: async (path) => await nodeReaddir(path),
125
165
  readFile: async (path, encoding) => await nodeReadFile(path, encoding),
166
+ readBytes: async (path) => await nodeReadFile(path),
126
167
  lstat: async (path) => await nodeLstat(path),
127
168
  writeFile: async (path, data, options) => {
128
169
  await nodeWriteFile(path, data, options);
@@ -139,6 +180,9 @@ const defaultFsDependencies = {
139
180
  rename: async (from, to) => {
140
181
  await nodeRename(from, to);
141
182
  },
183
+ link: async (path, dest) => {
184
+ await nodeLink(path, dest);
185
+ },
142
186
  unlink: async (path) => {
143
187
  await nodeUnlink(path);
144
188
  },
@@ -146,6 +190,28 @@ const defaultFsDependencies = {
146
190
  await nodeRmdir(path);
147
191
  },
148
192
  };
193
+ async function defaultRecoveryRenameNoReplace(from, to) {
194
+ if (!from.trim() || !to.trim() || !isAbsolute(from) || !isAbsolute(to))
195
+ return 'unsupported';
196
+ try {
197
+ const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['fs-rename-no-replace', from, to], {
198
+ encoding: 'utf8',
199
+ maxBuffer: 64 * 1024,
200
+ shell: false,
201
+ stdio: ['ignore', 'pipe', 'pipe'],
202
+ timeout: 10_000,
203
+ });
204
+ const parsed = JSON.parse(String(stdout));
205
+ if (parsed.outcome === 'moved' || parsed.outcome === 'not-moved' || parsed.outcome === 'unsupported') {
206
+ return parsed.outcome;
207
+ }
208
+ }
209
+ catch {
210
+ // Missing binaries, spawn failures, hard runtime errors, and malformed output
211
+ // all fail closed; recovery must never fall back to an overwriting rename.
212
+ }
213
+ return 'unsupported';
214
+ }
149
215
  /** @internal Exposed only so source-module tests can verify default ESRCH handling. */
150
216
  export function __createDefaultPidProbeForTests(killZero) {
151
217
  return (pid) => {
@@ -163,26 +229,40 @@ function defaultProbePid(pid) {
163
229
  process.kill(targetPid, 0);
164
230
  })(pid);
165
231
  }
166
- function defaultReadProcessIdentity(pid, platform) {
167
- if (platform !== 'linux')
168
- return { status: 'indeterminate' };
169
- const identity = readLinuxProcessIdentity(pid);
170
- if (!identity)
171
- return { status: 'indeterminate' };
172
- const cmdlineHash = hashCmdline(identity.cmdline);
173
- return {
174
- status: 'matching',
175
- startTicks: identity.startTicks,
176
- ...(cmdlineHash ? { cmdlineHash } : {}),
177
- };
232
+ function defaultObserveProcess(pid, platform) {
233
+ if (platform === 'linux') {
234
+ return observeLinuxProcess(pid);
235
+ }
236
+ if (platform !== 'darwin' && platform !== 'win32') {
237
+ return { kind: 'unsupported' };
238
+ }
239
+ try {
240
+ const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['process-identity', String(pid)], {
241
+ encoding: 'utf8',
242
+ timeout: 3000,
243
+ maxBuffer: 64 * 1024,
244
+ shell: false,
245
+ stdio: ['ignore', 'pipe', 'pipe'],
246
+ });
247
+ return parseProcessIdentityOutput(stdout);
248
+ }
249
+ catch {
250
+ return { kind: 'unsupported' };
251
+ }
178
252
  }
179
253
  const defaultTransactionDependencies = {
180
254
  fs: defaultFsDependencies,
181
255
  nowMs: () => Date.now(),
182
256
  sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
183
257
  token: () => randomUUID(),
258
+ runtimePlatform: process.platform,
184
259
  probePid: defaultProbePid,
185
- readProcessIdentity: defaultReadProcessIdentity,
260
+ observeProcess: defaultObserveProcess,
261
+ atomicRenameNoReplace: defaultRecoveryRenameNoReplace,
262
+ };
263
+ export const defaultProcessInspectionProvider = {
264
+ probePid: defaultProbePid,
265
+ observeProcess: defaultObserveProcess,
186
266
  };
187
267
  let transactionDependencies = defaultTransactionDependencies;
188
268
  /** @internal Source-module test harness. Always reset after a test. */
@@ -234,38 +314,344 @@ function readLinuxProcessIdentity(pid) {
234
314
  return null;
235
315
  }
236
316
  }
237
- function defaultIsPidAlive(pid) {
317
+ const NODE_PLATFORMS = new Set([
318
+ 'aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'openbsd', 'sunos', 'win32', 'cygwin', 'netbsd',
319
+ ]);
320
+ const PROCESS_BIRTH_PATTERN = /^\d+(?:\.\d+)?$/;
321
+ function isNodePlatform(value) {
322
+ return typeof value === 'string' && NODE_PLATFORMS.has(value);
323
+ }
324
+ function isValidBirth(value) {
325
+ return typeof value === 'string' && PROCESS_BIRTH_PATTERN.test(value);
326
+ }
327
+ export function isValidProcessIdentity(value) {
328
+ if (!value || typeof value !== 'object' || Array.isArray(value))
329
+ return false;
330
+ const identity = value;
331
+ if (Object.keys(identity).some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline_hash'))
332
+ return false;
333
+ return isNodePlatform(identity.platform)
334
+ && isValidBirth(identity.birth)
335
+ && (identity.cmdline_hash === undefined
336
+ || typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
337
+ }
338
+ function isComparableProcessIdentity(value) {
339
+ if (!value || typeof value !== 'object' || Array.isArray(value))
340
+ return false;
341
+ const identity = value;
342
+ return isNodePlatform(identity.platform)
343
+ && typeof identity.birth === 'string'
344
+ && identity.birth.length > 0
345
+ && (identity.cmdline_hash === undefined
346
+ || typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
347
+ }
348
+ function observeLinuxProcess(pid) {
349
+ if (!Number.isInteger(pid) || pid <= 0)
350
+ return { kind: 'error' };
351
+ const identity = readLinuxProcessIdentity(pid);
352
+ if (identity) {
353
+ const cmdlineHash = hashCmdline(identity.cmdline);
354
+ return {
355
+ kind: 'identity',
356
+ identity: {
357
+ platform: 'linux',
358
+ birth: String(identity.startTicks),
359
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
360
+ },
361
+ };
362
+ }
363
+ let statContent;
238
364
  try {
239
- process.kill(pid, 0);
240
- return true;
365
+ statContent = readFileSync(`/proc/${pid}/stat`, 'utf8');
366
+ }
367
+ catch (error) {
368
+ const code = errorCode(error);
369
+ if (code === 'ENOENT' || code === 'ESRCH')
370
+ return { kind: 'gone' };
371
+ if (code === 'EACCES' || code === 'EPERM')
372
+ return { kind: 'denied' };
373
+ return { kind: 'error' };
374
+ }
375
+ if (!statContent)
376
+ return { kind: 'error' };
377
+ const probe = defaultProbePid(pid);
378
+ if (probe === 'dead')
379
+ return { kind: 'gone' };
380
+ if (probe === 'indeterminate')
381
+ return { kind: 'denied' };
382
+ return { kind: 'error' };
383
+ }
384
+ function parseProcessIdentityOutput(stdout) {
385
+ try {
386
+ const parsed = JSON.parse(stdout);
387
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
388
+ return { kind: 'error' };
389
+ const value = parsed;
390
+ const keys = Object.keys(value);
391
+ const outcome = value.outcome;
392
+ if (outcome !== undefined) {
393
+ if (typeof outcome !== 'string')
394
+ return { kind: 'error' };
395
+ if (outcome === 'gone' || outcome === 'denied' || outcome === 'unsupported') {
396
+ if (keys.length !== 1)
397
+ return { kind: 'error' };
398
+ if (outcome === 'gone')
399
+ return { kind: 'gone' };
400
+ if (outcome === 'denied')
401
+ return { kind: 'denied' };
402
+ return { kind: 'unsupported' };
403
+ }
404
+ if (outcome === 'error')
405
+ return { kind: 'error' };
406
+ return { kind: 'error' };
407
+ }
408
+ if (keys.some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline')) {
409
+ return { kind: 'error' };
410
+ }
411
+ const platform = value.platform;
412
+ const birth = value.birth;
413
+ if (!isNodePlatform(platform) || !isValidBirth(birth))
414
+ return { kind: 'error' };
415
+ if (value.cmdline !== undefined && typeof value.cmdline !== 'string')
416
+ return { kind: 'error' };
417
+ const cmdlineHash = hashCmdline(value.cmdline);
418
+ return {
419
+ kind: 'identity',
420
+ identity: {
421
+ platform,
422
+ birth,
423
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
424
+ },
425
+ };
241
426
  }
242
427
  catch {
243
- return false;
428
+ return { kind: 'error' };
244
429
  }
245
430
  }
246
431
  /**
247
432
  * Legacy boolean stale check retained for read compatibility. The pointer
248
433
  * classifier below keeps indeterminate liveness distinct from definitely dead.
434
+ *
435
+ * KNOWN GAP (darwin): `identity-indeterminate` is treated as stale here, which is fail-closed and
436
+ * correct on platforms that DO record process-birth evidence but cannot read it. On darwin no
437
+ * identity source exists at all, so a live session also classifies indeterminate and reads as
438
+ * stale. Two suite cases pin that gap (`treats symlinked cwd aliases as authoritative`,
439
+ * `writes session start/end lifecycle artifacts`), while `returns true on Linux when identity
440
+ * metadata is missing` / `... cannot be read` pin the fail-closed behavior, so the two cannot be
441
+ * reconciled by changing this mapping. Fixing it requires distinguishing "no identity source on
442
+ * this platform" from "identity expected but unavailable" in `classifySessionProcess` itself.
443
+ * Callers needing a safe live-session answer today use `classifySessionStateLiveness(...) !==
444
+ * 'stale-dead'` (see src/cli/setup.ts overwrite guard).
249
445
  */
446
+ function sessionClassificationDependencies(options) {
447
+ const runtimePlatform = options.platform ?? transactionDependencies.runtimePlatform ?? process.platform;
448
+ const probePid = options.probePid ?? (options.isPidAlive
449
+ ? (pid) => options.isPidAlive(pid) ? 'alive' : 'dead'
450
+ : transactionDependencies.probePid);
451
+ const observeProcess = options.observeProcess ?? (options.readLinuxIdentity
452
+ ? (pid, platform) => {
453
+ if (platform !== 'linux')
454
+ return { kind: 'unsupported' };
455
+ const identity = options.readLinuxIdentity(pid);
456
+ if (!identity)
457
+ return { kind: 'error' };
458
+ const cmdlineHash = hashCmdline(identity.cmdline);
459
+ return {
460
+ kind: 'identity',
461
+ identity: {
462
+ platform: 'linux',
463
+ birth: String(identity.startTicks),
464
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
465
+ },
466
+ };
467
+ }
468
+ : transactionDependencies.observeProcess);
469
+ return { runtimePlatform, probePid, observeProcess };
470
+ }
250
471
  export function isSessionStale(state, options = {}) {
251
472
  if (!Number.isInteger(state.pid) || state.pid <= 0)
252
473
  return true;
253
- const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
254
- if (!isPidAlive(state.pid))
255
- return true;
256
- const platform = options.platform ?? process.platform;
257
- if (platform !== 'linux')
474
+ const { runtimePlatform, probePid, observeProcess } = sessionClassificationDependencies(options);
475
+ const dependencies = {
476
+ ...transactionDependencies,
477
+ runtimePlatform,
478
+ probePid,
479
+ observeProcess,
480
+ };
481
+ const classification = classifySessionProcess(state, dependencies);
482
+ if (classification === 'usable')
258
483
  return false;
259
- const liveIdentity = (options.readLinuxIdentity ?? readLinuxProcessIdentity)(state.pid);
260
- if (!liveIdentity || typeof state.pid_start_ticks !== 'number')
261
- return true;
262
- if (state.pid_start_ticks !== liveIdentity.startTicks)
484
+ if (classification === 'stale-dead')
263
485
  return true;
264
- const expectedCmdline = normalizeCmdline(state.pid_cmdline);
265
- if (!expectedCmdline)
486
+ // `identity-indeterminate` means two different things and only one of them is stale.
487
+ //
488
+ // Where the platform CAN record process-birth evidence (linux) but it is missing or unreadable,
489
+ // fail closed: that is the case `returns true on Linux when identity metadata is missing` and
490
+ // `... when live identity cannot be read` pin deliberately.
491
+ //
492
+ // Where no identity source exists at all, every live session classifies indeterminate, so failing
493
+ // closed reported a running session as dead. There a positively alive pid is the strongest
494
+ // evidence available, and this is a READ predicate, not an authority grant: the classifier still
495
+ // returns identity-indeterminate (pinned by `classifies identity-less live non-Linux pointers as
496
+ // identity-indeterminate`), and authority-granting paths keep requiring `usable`.
497
+ return !isUnobservableLivePointerState(state, runtimePlatform, probePid);
498
+ }
499
+ function safeObserve(provider, pid, runtimePlatform) {
500
+ try {
501
+ return provider.observeProcess(pid, runtimePlatform);
502
+ }
503
+ catch {
504
+ return { kind: 'error' };
505
+ }
506
+ }
507
+ function compareObservation(recorded, observation, runtimePlatform) {
508
+ if (!observation || typeof observation !== 'object' || observation.kind !== 'identity') {
509
+ return 'identity-unavailable';
510
+ }
511
+ const identity = observation.identity;
512
+ if (!isComparableProcessIdentity(identity) || identity.platform !== runtimePlatform) {
513
+ return 'identity-unavailable';
514
+ }
515
+ if (identity.birth !== recorded.birth)
516
+ return 'birth-mismatch-retryable';
517
+ if (recorded.cmdline_hash !== undefined && identity.cmdline_hash !== recorded.cmdline_hash) {
518
+ return 'identity-unavailable';
519
+ }
520
+ return 'match';
521
+ }
522
+ /** Compare recorded process birth evidence with a fresh provider observation. */
523
+ export function classifyRecordedIdentity(recorded, runtimePlatform, provider, pid) {
524
+ if (!isComparableProcessIdentity(recorded) || recorded.platform !== runtimePlatform || !Number.isInteger(pid) || pid <= 0) {
525
+ return { status: 'identity-unavailable' };
526
+ }
527
+ let probe;
528
+ try {
529
+ probe = provider.probePid(pid);
530
+ }
531
+ catch {
532
+ return { status: 'identity-unavailable' };
533
+ }
534
+ if (probe === 'dead')
535
+ return { status: 'gone' };
536
+ if (probe !== 'alive')
537
+ return { status: 'identity-unavailable' };
538
+ const first = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
539
+ if (first === 'birth-mismatch-retryable') {
540
+ const second = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
541
+ if (second === 'match')
542
+ return { status: 'match' };
543
+ if (second === 'birth-mismatch-retryable' || second === 'birth-mismatch')
544
+ return { status: 'birth-mismatch' };
545
+ return { status: 'identity-unavailable' };
546
+ }
547
+ if (first === 'match')
548
+ return { status: 'match' };
549
+ if (first === 'birth-mismatch')
550
+ return { status: 'birth-mismatch' };
551
+ return { status: 'identity-unavailable' };
552
+ }
553
+ function recordedIdentityForState(state, runtimePlatform) {
554
+ if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
555
+ return 'invalid';
556
+ if (state.process_identity !== undefined) {
557
+ if (state.identity_schema_version !== 2 || !isValidProcessIdentity(state.process_identity))
558
+ return 'invalid';
559
+ if (state.platform !== undefined && state.platform !== state.process_identity.platform)
560
+ return 'invalid';
561
+ return state.process_identity;
562
+ }
563
+ if (state.identity_schema_version === 2)
564
+ return 'invalid';
565
+ const recordedPlatform = state.platform ?? runtimePlatform;
566
+ if (recordedPlatform === 'linux' && isValidStartTicks(state.pid_start_ticks)) {
567
+ const cmdlineHash = hashCmdline(state.pid_cmdline);
568
+ return {
569
+ platform: 'linux',
570
+ birth: String(state.pid_start_ticks),
571
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
572
+ };
573
+ }
574
+ return null;
575
+ }
576
+ /**
577
+ * A state with no recorded identity is indeterminate unless the pid is positively dead.
578
+ *
579
+ * Do NOT relax this to "alive + non-linux => usable": `classifies identity-less live non-Linux
580
+ * pointers as identity-indeterminate` pins it deliberately, because without birth evidence an
581
+ * alive pid cannot be distinguished from a REUSED pid, and granting `usable` there would let a
582
+ * foreign process inherit session authority. Read paths that only need "is anyone alive here"
583
+ * must ask `classifySessionStateLiveness(...) !== 'stale-dead'` instead (see the overwrite guard
584
+ * in src/cli/setup.ts); authority-granting paths keep requiring `usable`.
585
+ */
586
+ function probeIdentitylessProcess(pid, dependencies) {
587
+ let probe;
588
+ try {
589
+ probe = dependencies.probePid(pid);
590
+ }
591
+ catch {
592
+ return 'identity-indeterminate';
593
+ }
594
+ return probe === 'dead' ? 'stale-dead' : 'identity-indeterminate';
595
+ }
596
+ /**
597
+ * Is an `identity-indeterminate` pointer merely UNOBSERVABLE on this platform, rather than
598
+ * untrustworthy?
599
+ *
600
+ * Three distinct situations collapse into `identity-indeterminate`, and only the first one describes
601
+ * a pointer that may legitimately be live:
602
+ * - the platform records no process-birth evidence at all (every live pointer lands here);
603
+ * - linux, which does record it, is missing or cannot read it (fail closed: the metadata should
604
+ * exist);
605
+ * - the evidence claims a DIFFERENT platform, which is forged or foreign (fail closed).
606
+ *
607
+ * Read predicates and preservation/refusal decisions may treat the first case as occupied. Authority
608
+ * to own writes still requires `usable`, and the classifier itself is unchanged.
609
+ */
610
+ function isUnobservableLivePointerState(state,
611
+ // Explicit rather than read from transactionDependencies: isSessionStale accepts injected platform
612
+ // and probe overrides, and silently using the host values there evaluated the linux fail-closed
613
+ // cases against the wrong runtime.
614
+ runtimePlatform = transactionDependencies.runtimePlatform, probe = transactionDependencies.probePid) {
615
+ if (!state)
616
+ return false;
617
+ const recordedPlatform = state.process_identity?.platform ?? state.platform ?? runtimePlatform;
618
+ if (recordedPlatform !== runtimePlatform)
619
+ return false;
620
+ if (recordedPlatform === 'linux')
621
+ return false;
622
+ if (typeof state.pid !== 'number' || !Number.isInteger(state.pid) || state.pid <= 0)
623
+ return false;
624
+ // POSITIVE liveness is required, not merely a plausible shape. A pid that cannot be probed at all
625
+ // (for example an out-of-range one) is not evidence of a live session, and admitting it would let
626
+ // a stale pointer survive a relaunch instead of failing closed.
627
+ try {
628
+ return probe(state.pid) === 'alive';
629
+ }
630
+ catch {
266
631
  return false;
267
- const liveCmdline = normalizeCmdline(liveIdentity.cmdline);
268
- return !liveCmdline || liveCmdline !== expectedCmdline;
632
+ }
633
+ }
634
+ /**
635
+ * May an existing pointer whose liveness could NOT be proven be reconciled in place?
636
+ *
637
+ * `isUnobservableLivePointerState` answers "someone may be here", which is enough to REFUSE a foreign
638
+ * claim or to preserve lineage, but it is not proof of WHO is here, so reconciling it is an adoption
639
+ * and needs its own justification.
640
+ *
641
+ * Neither pid NOR platform equality can be required here, and both were measured rather than assumed:
642
+ * native SessionStart reconciliation exists so a native Codex process can adopt a pointer an OMX
643
+ * launch wrote, so the pid legitimately differs, and `preserves canonical session id while
644
+ * reconciling native SessionStart metadata` also reconciles with a different announced platform.
645
+ * Requiring either refused legitimate reconciliation (that test fails), which is the defect this
646
+ * change set fixes.
647
+ *
648
+ * What bounds the adoption is therefore: this cwd-authority check, asserted locally so the invariant
649
+ * is visible at the mutation site instead of only implied by the classifier's foreign-cwd rejection;
650
+ * plus native-id compatibility and owner-alias merge in the caller. A pointer for another working
651
+ * directory or another session can never be adopted.
652
+ */
653
+ function mayAdoptUnprovenPointer(existing, cwd) {
654
+ return isSessionStateAuthoritativeForCwd(existing, cwd);
269
655
  }
270
656
  export function isSessionStateAuthoritativeForCwd(state, cwd) {
271
657
  if (!normalizeSessionId(state.session_id))
@@ -284,55 +670,49 @@ export function isSessionStateUsable(state, cwd, options = {}) {
284
670
  return false;
285
671
  if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
286
672
  return false;
287
- const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
288
- const hasLinuxIdentityMetadata = typeof state.pid_start_ticks === 'number'
289
- || typeof state.pid_cmdline === 'string';
290
- return !hasPidMetadata && !hasLinuxIdentityMetadata || !isSessionStale(state, options);
673
+ const { runtimePlatform, probePid, observeProcess } = sessionClassificationDependencies(options);
674
+ return classifySessionProcess(state, {
675
+ ...transactionDependencies,
676
+ runtimePlatform,
677
+ probePid,
678
+ observeProcess,
679
+ }) === 'usable';
291
680
  }
292
681
  function isValidStartTicks(value) {
293
682
  return typeof value === 'number' && Number.isInteger(value) && value >= 0;
294
683
  }
295
684
  function classifySessionProcess(state, dependencies) {
296
685
  const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
297
- const hasIdentityMetadata = typeof state.pid_start_ticks === 'number' || typeof state.pid_cmdline === 'string';
686
+ const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
687
+ || typeof state.pid_cmdline === 'string'
688
+ || state.identity_schema_version !== undefined
689
+ || state.process_identity !== undefined;
298
690
  if (!hasPidMetadata && !hasIdentityMetadata)
299
691
  return 'usable';
300
692
  if (!hasPidMetadata)
301
693
  return 'identity-indeterminate';
302
- let pidStatus;
303
- try {
304
- pidStatus = dependencies.probePid(state.pid);
305
- }
306
- catch {
307
- return 'identity-indeterminate';
308
- }
309
- if (pidStatus === 'dead')
310
- return 'stale-dead';
311
- if (pidStatus !== 'alive')
312
- return 'identity-indeterminate';
313
- const platform = state.platform ?? process.platform;
314
- if (platform !== 'linux')
315
- return 'usable';
316
- if (!isValidStartTicks(state.pid_start_ticks))
317
- return 'identity-indeterminate';
318
- let liveIdentity;
319
- try {
320
- liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
321
- }
322
- catch {
694
+ const runtimePlatform = dependencies.runtimePlatform;
695
+ const recorded = recordedIdentityForState(state, runtimePlatform);
696
+ if (recorded === 'invalid')
323
697
  return 'identity-indeterminate';
698
+ if (!recorded)
699
+ return probeIdentitylessProcess(state.pid, dependencies);
700
+ const classification = classifyRecordedIdentity(recorded, runtimePlatform, dependencies, state.pid);
701
+ switch (classification.status) {
702
+ case 'match': return 'usable';
703
+ case 'gone':
704
+ case 'birth-mismatch': return 'stale-dead';
705
+ case 'identity-unavailable': return 'identity-indeterminate';
324
706
  }
325
- if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks))
326
- return 'identity-indeterminate';
327
- if (liveIdentity.startTicks !== state.pid_start_ticks)
328
- return 'stale-dead';
329
- if (liveIdentity.status !== 'matching')
330
- return 'identity-indeterminate';
331
- const expectedCmdlineHash = hashCmdline(state.pid_cmdline);
332
- if (expectedCmdlineHash && liveIdentity.cmdlineHash !== expectedCmdlineHash) {
333
- return 'identity-indeterminate';
334
- }
335
- return 'usable';
707
+ }
708
+ /**
709
+ * Classify process liveness for an already-parsed selected session pointer.
710
+ * Callers that need cwd/state_root authority must validate those against the
711
+ * SAME snapshot themselves; this evaluates only pid/start-tick evidence so a
712
+ * single immutable snapshot can drive both decisions.
713
+ */
714
+ export function classifySessionStateLiveness(state) {
715
+ return classifySessionProcess(state, transactionDependencies);
336
716
  }
337
717
  function classifyParsedSessionPointer(context, value, raw) {
338
718
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -344,6 +724,16 @@ function classifyParsedSessionPointer(context, value, raw) {
344
724
  if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
345
725
  return { status: 'foreign-cwd', raw, state };
346
726
  }
727
+ if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
728
+ return { status: 'identity-indeterminate', raw, state };
729
+ if (state.process_identity !== undefined
730
+ && (state.identity_schema_version !== 2
731
+ || !isValidProcessIdentity(state.process_identity)
732
+ || state.platform !== undefined && state.platform !== state.process_identity.platform)) {
733
+ return { status: 'malformed', raw, state };
734
+ }
735
+ if (state.identity_schema_version === 2 && state.process_identity === undefined)
736
+ return { status: 'malformed', raw, state };
347
737
  const processStatus = classifySessionProcess(state, transactionDependencies);
348
738
  return { status: processStatus, state, raw };
349
739
  }
@@ -382,9 +772,9 @@ export async function readUsableSessionStateFromContext(context) {
382
772
  return state && isSessionStateUsable(state, context.cwd) ? state : null;
383
773
  }
384
774
  /** Read current session state from the exact selected root. */
385
- export async function readSessionState(cwd) {
775
+ export async function readSessionState(cwd, env = process.env) {
386
776
  try {
387
- return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
777
+ return await readSessionStateFromContext(resolveSessionPointerContext(cwd, env));
388
778
  }
389
779
  catch {
390
780
  return null;
@@ -400,7 +790,7 @@ export async function readUsableSessionState(cwd, options = {}) {
400
790
  return null;
401
791
  }
402
792
  }
403
- function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, options = {}) {
793
+ function createSessionState(cwd, stateRoot, sessionId, pid, platform, identity, options = {}) {
404
794
  const nowIso = options.nowIso ?? new Date().toISOString();
405
795
  const nativeSessionId = normalizeNonempty(options.nativeSessionId);
406
796
  const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
@@ -408,6 +798,27 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
408
798
  const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
409
799
  const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
410
800
  const tmuxPaneId = normalizeNonempty(options.tmuxPaneId);
801
+ const processIdentity = identity && 'birth' in identity
802
+ ? identity
803
+ : identity
804
+ ? {
805
+ platform: 'linux',
806
+ birth: String(identity.startTicks),
807
+ ...(hashCmdline(identity.cmdline) ? { cmdline_hash: hashCmdline(identity.cmdline) } : {}),
808
+ }
809
+ : undefined;
810
+ const publishIdentity = processIdentity && isValidProcessIdentity(processIdentity) && processIdentity.platform === platform
811
+ ? processIdentity
812
+ : undefined;
813
+ const legacyStartTicks = identity && 'startTicks' in identity
814
+ ? identity.startTicks
815
+ : publishIdentity?.platform === 'linux' && Number.isSafeInteger(Number(publishIdentity.birth))
816
+ ? Number(publishIdentity.birth)
817
+ : undefined;
818
+ const legacyLinuxIdentity = publishIdentity?.platform === 'linux' && publishIdentity.birth === String(legacyStartTicks)
819
+ ? readLinuxProcessIdentity(pid)
820
+ : null;
821
+ const legacyCmdline = identity && 'cmdline' in identity ? identity.cmdline : legacyLinuxIdentity?.cmdline;
411
822
  return {
412
823
  session_id: sessionId,
413
824
  ...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
@@ -415,15 +826,23 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
415
826
  ...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
416
827
  ...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
417
828
  started_at: options.startedAt ?? nowIso,
829
+ ...(isValidToken(options.launchLineageToken) ? { launch_lineage_token: options.launchLineageToken } : {}),
418
830
  cwd,
831
+ state_root: stateRoot,
419
832
  pid,
420
833
  platform,
421
- ...(linuxIdentity ? { pid_start_ticks: linuxIdentity.startTicks } : {}),
422
- ...(linuxIdentity?.cmdline ? { pid_cmdline: linuxIdentity.cmdline } : {}),
834
+ ...(publishIdentity ? { identity_schema_version: 2, process_identity: publishIdentity } : {}),
835
+ ...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
836
+ ...(legacyCmdline ? { pid_cmdline: legacyCmdline } : {}),
423
837
  ...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
424
838
  ...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
425
839
  };
426
840
  }
841
+ function preserveExistingLaunchLineageToken(existing, state) {
842
+ return existing && Object.hasOwn(existing, 'launch_lineage_token')
843
+ ? { ...state, launch_lineage_token: existing.launch_lineage_token }
844
+ : state;
845
+ }
427
846
  function normalizeNonempty(value) {
428
847
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
429
848
  }
@@ -431,7 +850,15 @@ function resolvePid(options) {
431
850
  return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
432
851
  }
433
852
  function sessionIdentityFor(pid, platform) {
434
- return platform === 'linux' ? readLinuxProcessIdentity(pid) : null;
853
+ try {
854
+ const observation = transactionDependencies.observeProcess(pid, platform);
855
+ return observation.kind === 'identity' && isValidProcessIdentity(observation.identity) && observation.identity.platform === platform
856
+ ? observation.identity
857
+ : null;
858
+ }
859
+ catch {
860
+ return null;
861
+ }
435
862
  }
436
863
  function currentOwnerAlias(state) {
437
864
  return normalizeSessionId(state.owner_omx_session_id);
@@ -458,29 +885,38 @@ function isStartCompatible(existing, requestedSessionId) {
458
885
  || existing.native_session_id === requestedSessionId
459
886
  || currentOwnerAlias(existing) === requestedSessionId;
460
887
  }
461
- function getOmxLaunchSessionId(state) {
462
- if (state.session_id.startsWith('omx-'))
463
- return state.session_id;
464
- const owner = currentOwnerAlias(state);
465
- return owner?.startsWith('omx-') ? owner : undefined;
466
- }
467
888
  function parseLockOwner(raw) {
468
889
  try {
469
- const value = JSON.parse(raw);
470
- if (!value || typeof value !== 'object' || Array.isArray(value))
890
+ const parsed = JSON.parse(raw);
891
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
471
892
  return null;
472
- if (value.version !== 1 || !isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
893
+ const value = parsed;
894
+ if (!isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
473
895
  return null;
474
- if (typeof value.platform !== 'string' || !value.platform || typeof value.created_at !== 'string' || !value.created_at) {
896
+ if (!isNodePlatform(value.platform) || typeof value.created_at !== 'string' || !value.created_at)
475
897
  return null;
898
+ if (value.version === 1) {
899
+ if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
900
+ return null;
901
+ if (value.pid_cmdline_hash !== undefined
902
+ && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
903
+ return null;
904
+ }
905
+ return value;
476
906
  }
477
- if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
478
- return null;
479
- if (value.pid_cmdline_hash !== undefined
480
- && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
481
- return null;
907
+ if (value.version === 2) {
908
+ const processIdentity = value.process_identity;
909
+ if (!isValidProcessIdentity(processIdentity) || processIdentity.platform !== value.platform)
910
+ return null;
911
+ if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
912
+ return null;
913
+ if (value.pid_cmdline_hash !== undefined
914
+ && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
915
+ return null;
916
+ }
917
+ return value;
482
918
  }
483
- return value;
919
+ return null;
484
920
  }
485
921
  catch {
486
922
  return null;
@@ -489,6 +925,17 @@ function parseLockOwner(raw) {
489
925
  function isValidToken(value) {
490
926
  return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
491
927
  }
928
+ function recordedIdentityForLockOwner(owner) {
929
+ if (owner.version === 2)
930
+ return owner.process_identity;
931
+ if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks))
932
+ return null;
933
+ return {
934
+ platform: 'linux',
935
+ birth: String(owner.pid_start_ticks),
936
+ ...(owner.pid_cmdline_hash ? { cmdline_hash: owner.pid_cmdline_hash } : {}),
937
+ };
938
+ }
492
939
  async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
493
940
  let raw;
494
941
  try {
@@ -500,38 +947,32 @@ async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
500
947
  const owner = parseLockOwner(raw);
501
948
  if (!owner)
502
949
  return { status: 'malformed' };
503
- let pidStatus;
504
- try {
505
- pidStatus = transactionDependencies.probePid(owner.pid);
506
- }
507
- catch {
508
- return { status: 'identity-indeterminate', owner };
509
- }
510
- if (pidStatus === 'dead')
511
- return { status: 'dead', owner };
512
- if (pidStatus !== 'alive')
513
- return { status: 'identity-indeterminate', owner };
514
- if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks)) {
515
- return { status: 'identity-indeterminate', owner };
516
- }
517
- let liveIdentity;
518
- try {
519
- liveIdentity = transactionDependencies.readProcessIdentity(owner.pid, owner.platform);
520
- }
521
- catch {
522
- return { status: 'identity-indeterminate', owner };
523
- }
524
- if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks)) {
950
+ const recorded = recordedIdentityForLockOwner(owner);
951
+ if (!recorded) {
952
+ let probe;
953
+ try {
954
+ probe = transactionDependencies.probePid(owner.pid);
955
+ }
956
+ catch {
957
+ return { status: 'identity-indeterminate', owner };
958
+ }
959
+ if (probe === 'dead')
960
+ return { status: 'dead', owner };
961
+ // Same read-vs-authority split as the pointer path: where the platform records no process-birth
962
+ // evidence, an alive pid is the strongest signal available and the lock is occupied. Reaping
963
+ // still requires a positively 'dead' owner (safeToRecover === 'dead'), so nothing becomes
964
+ // reclaimable here; only "is this lock held" answers correctly.
965
+ if (probe === 'alive' && isUnobservableLivePointerState(owner))
966
+ return { status: 'live', owner };
525
967
  return { status: 'identity-indeterminate', owner };
526
968
  }
527
- if (liveIdentity.startTicks !== owner.pid_start_ticks)
528
- return { status: 'reused', owner };
529
- if (liveIdentity.status !== 'matching')
530
- return { status: 'identity-indeterminate', owner };
531
- if (owner.pid_cmdline_hash && owner.pid_cmdline_hash !== liveIdentity.cmdlineHash) {
532
- return { status: 'identity-indeterminate', owner };
969
+ const classification = classifyRecordedIdentity(recorded, transactionDependencies.runtimePlatform, transactionDependencies, owner.pid);
970
+ switch (classification.status) {
971
+ case 'match': return { status: 'live', owner };
972
+ case 'gone': return { status: 'dead', owner };
973
+ case 'birth-mismatch': return { status: 'reused', owner };
974
+ case 'identity-unavailable': return { status: 'identity-indeterminate', owner };
533
975
  }
534
- return { status: 'live', owner };
535
976
  }
536
977
  async function inspectLockOwner(lockPath) {
537
978
  return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
@@ -586,124 +1027,674 @@ async function inspectSessionPointerLockAtContext(context) {
586
1027
  status,
587
1028
  lockPath: context.lockPath,
588
1029
  evidenceSource,
589
- safeToRecover: status === 'dead' && evidenceSource === 'owner-temp',
1030
+ // Recoverable only on positively dead evidence (ESRCH), for canonical
1031
+ // owner.json and pre-rename temp evidence alike; the atomic
1032
+ // claim/quarantine protocol revalidates the exact entry, still-dead
1033
+ // status, and owner token before quarantining. Live, reused,
1034
+ // identity-indeterminate, ambiguous, symlink, malformed, unexpected,
1035
+ // and io-error states stay fail-closed.
1036
+ safeToRecover: status === 'dead',
590
1037
  };
591
1038
  }
592
1039
  /** Inspect only exact, regular owner evidence in the selected pointer lock. */
593
1040
  export async function inspectSessionPointerLock(cwd) {
594
1041
  return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
595
1042
  }
596
- /** Explicitly quarantine only a dead, atomically claimed pointer lock; never force recovery. */
597
- export async function recoverSessionPointerLock(cwd) {
598
- const context = resolveSessionPointerContext(cwd);
599
- const inspection = await inspectSessionPointerLockAtContext(context);
600
- if (!inspection.safeToRecover) {
601
- return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
1043
+ function sameRecoveryIdentity(stat, identity, kind) {
1044
+ return !stat.isSymbolicLink() && (kind === 'file' ? stat.isFile() : stat.isDirectory()) && stat.dev === identity.dev && stat.ino === identity.ino;
1045
+ }
1046
+ async function lstatRecoveryPath(path) {
1047
+ try {
1048
+ return await transactionDependencies.fs.lstat(path);
602
1049
  }
603
- let entries;
1050
+ catch (error) {
1051
+ if (isNotFound(error))
1052
+ return undefined;
1053
+ throw error;
1054
+ }
1055
+ }
1056
+ async function probeRecoveryRenameCapability() {
1057
+ let probeDir;
604
1058
  try {
605
- entries = await transactionDependencies.fs.readdir(context.lockPath);
1059
+ probeDir = await nodeMkdtemp(join(tmpdir(), 'omx-session-recovery-probe-'));
1060
+ const from = join(probeDir, 'from');
1061
+ const to = join(probeDir, 'to');
1062
+ await nodeWriteFile(from, 'omx-recovery-capability-probe', { flag: 'wx' });
1063
+ const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
1064
+ return outcome === 'moved' || outcome === 'not-moved' || outcome === 'unsupported'
1065
+ ? outcome
1066
+ : 'unsupported';
606
1067
  }
607
1068
  catch {
608
- return { ...inspection, action: 'none', recovered: false, reason: 'Unable to re-read session pointer lock evidence.' };
1069
+ return 'unsupported';
609
1070
  }
610
- const evidenceName = inspection.evidenceSource === 'owner.json' ? 'owner.json' : entries.find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
611
- if (!evidenceName || entries.length !== 1) {
612
- return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1071
+ finally {
1072
+ if (probeDir) {
1073
+ try {
1074
+ await rm(probeDir, { recursive: true, force: true });
1075
+ }
1076
+ catch { /* best effort */ }
1077
+ }
613
1078
  }
614
- const owner = await inspectLockOwnerFile(join(context.lockPath, evidenceName));
615
- if (owner.status !== 'dead' || !owner.owner) {
616
- return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1079
+ }
1080
+ async function moveRecoveryPathNoReplace(from, to, identity, kind, onAtomicMove) {
1081
+ try {
1082
+ const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
1083
+ if (outcome === 'unsupported')
1084
+ return { moved: false, unsupported: true, reason: 'Atomic no-replace recovery rename is unsupported on this platform.' };
1085
+ if (outcome === 'not-moved')
1086
+ return { moved: false, reason: 'Atomic recovery move left its source pathname in place.' };
1087
+ onAtomicMove?.();
617
1088
  }
618
- let claimToken;
1089
+ catch (error) {
1090
+ return { moved: false, reason: `Atomic no-replace recovery rename failed (${errorCode(error) ?? 'unknown'}).` };
1091
+ }
1092
+ const destination = await lstatRecoveryPath(to);
1093
+ const source = await lstatRecoveryPath(from);
1094
+ if (destination && sameRecoveryIdentity(destination, identity, kind) && !source)
1095
+ return { moved: true };
1096
+ // A successor may have appeared at the canonical pathname after our move.
1097
+ // Attempt a no-replace rollback only for the captured object; an occupied or
1098
+ // foreign destination makes the rollback fail closed and leaves both residues.
1099
+ if (destination && sameRecoveryIdentity(destination, identity, kind)) {
1100
+ try {
1101
+ await transactionDependencies.atomicRenameNoReplace(to, from);
1102
+ }
1103
+ catch { /* durable residue is safer than cleanup */ }
1104
+ }
1105
+ return { moved: false, reason: 'Atomic recovery move did not leave the captured object at its token-bound destination.' };
1106
+ }
1107
+ async function completeRecoveryCheckpoint(checkpointPath, checkpointBytes, checkpointIdentity) {
1108
+ const completedPath = checkpointPath.replace(/\.json$/, '.completed');
1109
+ const current = await lstatRecoveryPath(checkpointPath);
1110
+ if (!current || !sameRecoveryIdentity(current, checkpointIdentity, 'file') || await transactionDependencies.fs.readFile(checkpointPath, 'utf8') !== checkpointBytes) {
1111
+ return { completed: false, reason: 'Recovery checkpoint was replaced before completion; it was preserved as residue.' };
1112
+ }
1113
+ const moved = await moveRecoveryPathNoReplace(checkpointPath, completedPath, checkpointIdentity, 'file');
1114
+ if (!moved.moved)
1115
+ return { completed: false, reason: moved.reason };
1116
+ const completedBytes = await transactionDependencies.fs.readFile(completedPath, 'utf8');
1117
+ return completedBytes === checkpointBytes ? { completed: true } : { completed: false, reason: 'Completed recovery receipt bytes changed and were preserved as residue.' };
1118
+ }
1119
+ async function resumeRecoveryCheckpoint(context, checkpointPath, checkpointName) {
1120
+ const match = new RegExp(`^${basename(context.lockPath).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.recovery\\.([A-Za-z0-9_-]{16,128})\\.([A-Za-z0-9_-]{16,128})\\.json$`).exec(checkpointName);
619
1121
  try {
620
- claimToken = transactionDependencies.token();
1122
+ const checkpointBytes = await transactionDependencies.fs.readFile(checkpointPath, 'utf8');
1123
+ const checkpointStat = await transactionDependencies.fs.lstat(checkpointPath);
1124
+ const checkpoint = JSON.parse(checkpointBytes);
1125
+ const ownerToken = match?.[1];
1126
+ const recoveryToken = match?.[2];
1127
+ const expectedSource = ownerToken && (checkpoint.sourcePath === join(context.lockPath, 'owner.json') || checkpoint.sourcePath === join(context.lockPath, `owner.${ownerToken}.tmp`));
1128
+ const expectedPark = `${context.lockPath}.parked-lock-${recoveryToken}`;
1129
+ const isLegacyV1 = checkpoint.version === 1 && checkpoint.lockIdentity === undefined && checkpoint.lockParkPath === undefined;
1130
+ if (!match || !expectedSource || !checkpoint.identity || !['evidence-pending', 'evidence-quarantined', 'directory-pending'].includes(checkpoint.phase) || ![1, 2, 3].includes(checkpoint.version) || !isLegacyV1 && checkpoint.lockParkPath !== expectedPark || !sameRecoveryIdentity(checkpointStat, { dev: checkpointStat.dev, ino: checkpointStat.ino }, 'file'))
1131
+ throw new Error('invalid checkpoint');
1132
+ const lockParkPath = checkpoint.lockParkPath ?? expectedPark;
1133
+ const evidenceIdentity = checkpoint.evidenceIdentity ?? checkpoint.identity;
1134
+ const claimPath = join(context.lockPath, `owner.${ownerToken}.${recoveryToken}.recovery`);
1135
+ const lock = await lstatRecoveryPath(context.lockPath);
1136
+ const parkedLock = await lstatRecoveryPath(lockParkPath);
1137
+ if (lock && parkedLock)
1138
+ throw new Error('checkpoint lock paths are both present');
1139
+ if (parkedLock) {
1140
+ if (!checkpoint.lockIdentity || !sameRecoveryIdentity(parkedLock, checkpoint.lockIdentity, 'directory'))
1141
+ throw new Error('checkpoint parked lock identity mismatch');
1142
+ const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
1143
+ const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
1144
+ if (!completed.completed)
1145
+ throw new Error(completed.reason);
1146
+ return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovery checkpoint resumed.', quarantinePath };
1147
+ }
1148
+ if (!lock || lock.isSymbolicLink() || !lock.isDirectory())
1149
+ throw new Error('checkpoint lock is missing or foreign');
1150
+ const source = await lstatRecoveryPath(checkpoint.sourcePath);
1151
+ const claim = await lstatRecoveryPath(claimPath);
1152
+ const evidencePath = source && sameRecoveryIdentity(source, evidenceIdentity, 'file') ? checkpoint.sourcePath
1153
+ : claim && sameRecoveryIdentity(claim, evidenceIdentity, 'file') ? claimPath : undefined;
1154
+ if (!evidencePath)
1155
+ throw new Error('checkpoint evidence is missing or foreign');
1156
+ if (claim && !sameRecoveryIdentity(claim, evidenceIdentity, 'file'))
1157
+ throw new Error('checkpoint claim is foreign');
1158
+ // v1/v2 did not persist the directory identity. It is safe to derive only
1159
+ // while the original directory still contains the exact recorded evidence.
1160
+ const lockIdentity = checkpoint.lockIdentity ?? { dev: lock.dev, ino: lock.ino };
1161
+ if (!sameRecoveryIdentity(lock, lockIdentity, 'directory'))
1162
+ throw new Error('checkpoint lock identity mismatch');
1163
+ const ownerBytes = await transactionDependencies.fs.readFile(evidencePath, 'utf8');
1164
+ const owner = await inspectLockOwnerFile(evidencePath);
1165
+ if (owner.status !== 'dead' || owner.owner?.token !== ownerToken || checkpoint.evidenceBytes !== undefined && checkpoint.evidenceBytes !== ownerBytes)
1166
+ throw new Error('checkpoint owner evidence changed');
1167
+ const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
1168
+ const quarantine = await lstatRecoveryPath(quarantinePath);
1169
+ if (quarantine) {
1170
+ if (!sameRecoveryIdentity(quarantine, evidenceIdentity, 'file'))
1171
+ throw new Error('checkpoint quarantine is foreign');
1172
+ }
1173
+ else {
1174
+ await transactionDependencies.fs.link(evidencePath, quarantinePath);
1175
+ const linked = await lstatRecoveryPath(quarantinePath);
1176
+ if (!linked || !sameRecoveryIdentity(linked, evidenceIdentity, 'file'))
1177
+ throw new Error('checkpoint quarantine link mismatch');
1178
+ }
1179
+ // Revalidate the bytes, dead owner and identities immediately before moving
1180
+ // the entire directory incarnation out of the canonical lock pathname.
1181
+ const currentLock = await lstatRecoveryPath(context.lockPath);
1182
+ const currentEvidence = await lstatRecoveryPath(evidencePath);
1183
+ const currentBytes = currentEvidence && sameRecoveryIdentity(currentEvidence, evidenceIdentity, 'file') ? await transactionDependencies.fs.readFile(evidencePath, 'utf8') : undefined;
1184
+ const currentOwner = currentBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
1185
+ if (!currentLock || !sameRecoveryIdentity(currentLock, lockIdentity, 'directory') || currentBytes !== ownerBytes || currentOwner?.status !== 'dead' || currentOwner.owner?.token !== ownerToken)
1186
+ throw new Error('checkpoint owner evidence changed before directory quarantine');
1187
+ const lockEntries = await transactionDependencies.fs.readdir(context.lockPath);
1188
+ const allowedEntries = new Set([basename(checkpoint.sourcePath), basename(claimPath)]);
1189
+ if (lockEntries.some((entry) => !allowedEntries.has(entry)))
1190
+ throw new Error('checkpoint lock contains foreign recovery evidence');
1191
+ const moved = await moveRecoveryPathNoReplace(context.lockPath, lockParkPath, lockIdentity, 'directory');
1192
+ if (!moved.moved)
1193
+ throw new Error(moved.reason);
1194
+ const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
1195
+ if (!completed.completed)
1196
+ throw new Error(completed.reason);
1197
+ return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovered into durable quarantine residues.', quarantinePath };
621
1198
  }
622
- catch {
623
- return { ...inspection, action: 'none', recovered: false, reason: 'Unable to create recovery claim token.' };
1199
+ catch (error) {
1200
+ const detail = error instanceof Error ? error.message : 'unknown';
1201
+ return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: `Recovery checkpoint is malformed or no longer identifies its recorded recovery state (${detail}).` };
624
1202
  }
625
- if (!isValidToken(claimToken))
626
- return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
627
- const claimName = `owner.${owner.owner.token}.${claimToken}.recovery`;
628
- const claimPath = join(context.lockPath, claimName);
1203
+ }
1204
+ /** Explicitly quarantine only a dead lock by moving its entire directory incarnation. */
1205
+ export async function recoverSessionPointerLock(cwd) {
1206
+ const context = resolveSessionPointerContext(cwd);
1207
+ const checkpointPrefix = `${basename(context.lockPath)}.recovery.`;
1208
+ let checkpointNames;
629
1209
  try {
630
- await transactionDependencies.fs.lstat(claimPath);
631
- return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim path already exists.' };
1210
+ checkpointNames = (await transactionDependencies.fs.readdir(dirname(context.lockPath))).filter((name) => name.startsWith(checkpointPrefix) && name.endsWith('.json'));
632
1211
  }
633
1212
  catch (error) {
634
1213
  if (!isNotFound(error))
635
- return { ...inspection, action: 'none', recovered: false, reason: 'Unable to inspect recovery claim path.' };
1214
+ return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Unable to enumerate session pointer recovery checkpoints.' };
1215
+ checkpointNames = [];
1216
+ }
1217
+ if (checkpointNames.length > 1)
1218
+ return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Multiple recovery checkpoints exist.' };
1219
+ if (checkpointNames.length === 1)
1220
+ return await resumeRecoveryCheckpoint(context, join(dirname(context.lockPath), checkpointNames[0]), checkpointNames[0]);
1221
+ const inspection = await inspectSessionPointerLockAtContext(context);
1222
+ if (!inspection.safeToRecover)
1223
+ return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
1224
+ try {
1225
+ const evidenceName = inspection.evidenceSource === 'owner.json'
1226
+ ? 'owner.json'
1227
+ : (await transactionDependencies.fs.readdir(context.lockPath)).find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
1228
+ if (!evidenceName)
1229
+ return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1230
+ const evidencePath = join(context.lockPath, evidenceName);
1231
+ const evidence = await lstatRecoveryPath(evidencePath);
1232
+ const lock = await lstatRecoveryPath(context.lockPath);
1233
+ const ownerBytes = evidence && sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
1234
+ ? await transactionDependencies.fs.readFile(evidencePath, 'utf8')
1235
+ : undefined;
1236
+ const owner = ownerBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
1237
+ if (!lock || lock.isSymbolicLink() || !lock.isDirectory() || !evidence
1238
+ || !sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
1239
+ || owner?.status !== 'dead' || !owner.owner) {
1240
+ return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1241
+ }
1242
+ // Fresh recovery probes before checkpoint creation; an unsupported host leaves the
1243
+ // canonical lock and owner evidence untouched. Existing checkpoints are resumed
1244
+ // independently and may remain as durable recovery residue.
1245
+ const capability = await probeRecoveryRenameCapability();
1246
+ if (capability === 'unsupported') {
1247
+ return {
1248
+ ...inspection,
1249
+ action: 'none',
1250
+ recovered: false,
1251
+ reason: 'Atomic no-replace recovery rename is unsupported on this platform.',
1252
+ };
1253
+ }
1254
+ const recoveryToken = transactionDependencies.token();
1255
+ if (!isValidToken(recoveryToken))
1256
+ return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
1257
+ const checkpointPath = `${context.lockPath}.recovery.${owner.owner.token}.${recoveryToken}.json`;
1258
+ const checkpoint = {
1259
+ version: 3,
1260
+ sourcePath: evidencePath,
1261
+ identity: { dev: evidence.dev, ino: evidence.ino },
1262
+ evidenceIdentity: { dev: evidence.dev, ino: evidence.ino },
1263
+ evidenceBytes: ownerBytes,
1264
+ lockIdentity: { dev: lock.dev, ino: lock.ino },
1265
+ lockParkPath: `${context.lockPath}.parked-lock-${recoveryToken}`,
1266
+ phase: 'evidence-pending',
1267
+ };
1268
+ try {
1269
+ await transactionDependencies.fs.writeFile(checkpointPath, JSON.stringify(checkpoint), { flag: 'wx' });
1270
+ }
1271
+ catch (error) {
1272
+ return { ...inspection, action: 'none', recovered: false, reason: `Unable to create recovery checkpoint (${errorCode(error) ?? 'unknown'}).` };
1273
+ }
1274
+ return await resumeRecoveryCheckpoint(context, checkpointPath, basename(checkpointPath));
1275
+ }
1276
+ catch (error) {
1277
+ return {
1278
+ ...inspection,
1279
+ status: 'io-error',
1280
+ safeToRecover: false,
1281
+ action: 'none',
1282
+ recovered: false,
1283
+ reason: `Unable to inspect session pointer lock recovery state (${errorCode(error) ?? errorMessage(error)}).`,
1284
+ };
636
1285
  }
1286
+ }
1287
+ function selectedRootAuthorityFailure(context, state) {
1288
+ let recordedCwd;
1289
+ let observedCwd;
637
1290
  try {
638
- await transactionDependencies.fs.rename(join(context.lockPath, evidenceName), claimPath);
1291
+ if (typeof state.cwd !== 'string' || !state.cwd.trim())
1292
+ return 'foreign-cwd';
1293
+ recordedCwd = realpathSync(resolve(state.cwd));
1294
+ observedCwd = realpathSync(resolve(context.cwd));
1295
+ const cwdRelative = relative(recordedCwd, observedCwd);
1296
+ if (cwdRelative === '..' || cwdRelative.startsWith(`..${sep}`) || isAbsolute(cwdRelative))
1297
+ return 'foreign-cwd';
639
1298
  }
640
1299
  catch {
641
- return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1300
+ return 'foreign-cwd';
642
1301
  }
643
- const claimed = await inspectSessionPointerLockAtContext(context);
644
- let claimedEntries;
645
1302
  try {
646
- claimedEntries = await transactionDependencies.fs.readdir(context.lockPath);
1303
+ const recordedRoot = typeof state.state_root === 'string' && state.state_root.trim()
1304
+ ? state.state_root
1305
+ : join(recordedCwd, '.omx', 'state');
1306
+ return sameFilePath(recordedRoot, context.baseStateDir) ? undefined : 'foreign-root';
647
1307
  }
648
1308
  catch {
649
- return { ...claimed, action: 'none', recovered: false, reason: 'Unable to revalidate recovery claim.' };
1309
+ return 'foreign-root';
1310
+ }
1311
+ }
1312
+ function classifyParsedSessionPointerForRecovery(value, raw) {
1313
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1314
+ return { status: 'malformed', raw };
1315
+ const state = value;
1316
+ if (!normalizeSessionId(state.session_id))
1317
+ return { status: 'malformed', raw };
1318
+ if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2) {
1319
+ return { status: 'identity-indeterminate', raw, state };
650
1320
  }
651
- const claimOwner = await inspectLockOwnerFile(claimPath);
652
- if (claimedEntries.length !== 1 || claimedEntries[0] !== claimName || claimOwner.status !== 'dead' || claimOwner.owner?.token !== owner.owner.token || claimed.status !== 'unexpected') {
653
- return { ...claimed, action: 'none', recovered: false, reason: 'Recovery claim revalidation failed.' };
1321
+ if (state.process_identity !== undefined
1322
+ && (state.identity_schema_version !== 2
1323
+ || !isValidProcessIdentity(state.process_identity)
1324
+ || state.platform !== undefined && state.platform !== state.process_identity.platform)) {
1325
+ return { status: 'malformed', raw, state };
654
1326
  }
655
- const quarantinePath = `${context.lockPath}.quarantine.${owner.owner.token}.${claimToken}`;
1327
+ if (state.identity_schema_version === 2 && state.process_identity === undefined)
1328
+ return { status: 'malformed', raw, state };
1329
+ return { status: classifySessionProcess(state, transactionDependencies), raw, state };
1330
+ }
1331
+ async function readSessionPointerForRecovery(context) {
1332
+ let sourceStat;
656
1333
  try {
657
- await transactionDependencies.fs.lstat(quarantinePath);
658
- return { ...claimed, action: 'none', recovered: false, reason: 'Recovery quarantine path already exists.' };
1334
+ sourceStat = await transactionDependencies.fs.lstat(context.sessionPath);
659
1335
  }
660
1336
  catch (error) {
661
- if (!isNotFound(error))
662
- return { ...claimed, action: 'none', recovered: false, reason: 'Unable to inspect recovery quarantine path.' };
1337
+ if (isNotFound(error))
1338
+ return { status: 'absent' };
1339
+ throw error;
663
1340
  }
1341
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile())
1342
+ return { status: 'malformed' };
1343
+ const bytes = await transactionDependencies.fs.readBytes(context.sessionPath);
1344
+ const sourceAfterRead = await transactionDependencies.fs.lstat(context.sessionPath);
1345
+ if (!sameRecoveryIdentity(sourceAfterRead, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')) {
1346
+ throw new Error('Selected session pointer changed while its recovery snapshot was read.');
1347
+ }
1348
+ const raw = bytes.toString('utf8');
664
1349
  try {
665
- await transactionDependencies.fs.rename(claimPath, quarantinePath);
1350
+ return { ...classifyParsedSessionPointerForRecovery(JSON.parse(raw), raw), bytes };
666
1351
  }
667
- catch {
668
- return { ...claimed, action: 'none', recovered: false, reason: 'Unable to quarantine the exact session pointer recovery claim.' };
1352
+ catch (error) {
1353
+ if (error instanceof SyntaxError)
1354
+ return { status: 'malformed', raw, bytes };
1355
+ throw error;
1356
+ }
1357
+ }
1358
+ function malformedRecoveryPointerReason(state) {
1359
+ if (!normalizeSessionId(state.session_id))
1360
+ return 'session_id is missing or invalid';
1361
+ if (typeof state.started_at !== 'string' || !state.started_at.trim() || !Number.isFinite(Date.parse(state.started_at))) {
1362
+ return 'started_at is missing or invalid';
1363
+ }
1364
+ if (typeof state.cwd !== 'string' || !state.cwd.trim())
1365
+ return 'cwd is missing or invalid';
1366
+ if (!Number.isInteger(state.pid) || state.pid <= 0)
1367
+ return 'pid is missing or invalid';
1368
+ if (Object.prototype.hasOwnProperty.call(state, 'state_root')
1369
+ && (typeof state.state_root !== 'string' || !state.state_root.trim()))
1370
+ return 'state_root is invalid';
1371
+ if (state.platform !== undefined && !['linux', 'darwin', 'win32'].includes(state.platform))
1372
+ return 'platform is invalid';
1373
+ if (state.pid_start_ticks !== undefined && !isValidStartTicks(state.pid_start_ticks))
1374
+ return 'pid_start_ticks is invalid';
1375
+ if (state.pid_cmdline !== undefined && typeof state.pid_cmdline !== 'string')
1376
+ return 'pid_cmdline is invalid';
1377
+ const optionalStrings = [
1378
+ 'native_session_id',
1379
+ 'previous_native_session_id',
1380
+ 'native_session_switched_at',
1381
+ 'owner_omx_session_id',
1382
+ 'owner_codex_session_id',
1383
+ 'codex_session_id',
1384
+ 'tmux_session_name',
1385
+ 'tmux_pane_id',
1386
+ ];
1387
+ for (const field of optionalStrings) {
1388
+ const value = state[field];
1389
+ if (value !== undefined && (typeof value !== 'string' || !value.trim()))
1390
+ return `${field} is invalid`;
1391
+ }
1392
+ if (state.launch_lineage_token !== undefined && !isValidToken(state.launch_lineage_token)) {
1393
+ return 'launch_lineage_token is invalid';
669
1394
  }
1395
+ if (recordedIdentityForState(state, transactionDependencies.runtimePlatform) === 'invalid')
1396
+ return 'process identity metadata is invalid';
1397
+ return undefined;
1398
+ }
1399
+ function classifySessionProcessForRecovery(state) {
1400
+ const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
1401
+ const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
1402
+ || typeof state.pid_cmdline === 'string'
1403
+ || state.identity_schema_version !== undefined
1404
+ || state.process_identity !== undefined;
1405
+ if (!hasPidMetadata && !hasIdentityMetadata)
1406
+ return 'usable';
1407
+ if (!hasPidMetadata)
1408
+ return 'identity-indeterminate';
1409
+ const recordedPlatform = state.process_identity?.platform ?? state.platform;
1410
+ if (recordedPlatform !== undefined && recordedPlatform !== transactionDependencies.runtimePlatform) {
1411
+ return 'identity-indeterminate';
1412
+ }
1413
+ const recorded = recordedIdentityForState(state, transactionDependencies.runtimePlatform);
1414
+ if (recorded === 'invalid')
1415
+ return 'identity-indeterminate';
1416
+ if (!recorded) {
1417
+ let probe;
1418
+ try {
1419
+ probe = transactionDependencies.probePid(state.pid);
1420
+ }
1421
+ catch {
1422
+ return 'identity-indeterminate';
1423
+ }
1424
+ return probe === 'dead' ? 'dead' : 'identity-indeterminate';
1425
+ }
1426
+ const classification = classifyRecordedIdentity(recorded, transactionDependencies.runtimePlatform, transactionDependencies, state.pid);
1427
+ if (classification.status === 'gone')
1428
+ return 'dead';
1429
+ if (classification.status === 'birth-mismatch')
1430
+ return 'reused';
1431
+ if (classification.status === 'match')
1432
+ return 'usable';
1433
+ return 'identity-indeterminate';
1434
+ }
1435
+ function pointerRecoveryRefusal(context, status, reason, sessionId) {
1436
+ return {
1437
+ status,
1438
+ pointerPath: context.sessionPath,
1439
+ action: 'none',
1440
+ recovered: false,
1441
+ reason,
1442
+ ...(sessionId ? { sessionId } : {}),
1443
+ };
1444
+ }
1445
+ /**
1446
+ * Explicitly quarantine one authoritative, positively dead selected pointer.
1447
+ * The canonical pointer lock serializes the final classification and move with
1448
+ * SessionStart/SessionEnd, while the no-replace archive preserves exact bytes.
1449
+ */
1450
+ export async function recoverDeadSessionPointer(cwd) {
1451
+ const context = resolveSessionPointerContext(cwd);
670
1452
  try {
671
- await transactionDependencies.fs.rmdir(context.lockPath);
1453
+ if ((await readSessionPointerForRecovery(context)).status === 'absent') {
1454
+ return pointerRecoveryRefusal(context, 'absent', 'No selected session pointer exists.');
1455
+ }
672
1456
  }
673
1457
  catch (error) {
674
- if (!isNotFound(error)) {
675
- return {
676
- ...claimed,
677
- action: 'none',
678
- recovered: false,
679
- reason: 'The exact recovery claim was quarantined, but the canonical lock directory was not empty.',
1458
+ return pointerRecoveryRefusal(context, 'io-error', `Unable to read the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`);
1459
+ }
1460
+ const tracker = { degraded: false };
1461
+ let lock;
1462
+ try {
1463
+ lock = await acquirePointerLock(context, undefined, DEFAULT_POINTER_TIMEOUT_MS, process.platform, tracker);
1464
+ }
1465
+ catch (error) {
1466
+ const reason = isSessionPointerLaunchAbort(error) ? error.reason : errorMessage(error);
1467
+ return pointerRecoveryRefusal(context, 'lock-unavailable', `Unable to acquire the selected pointer transaction lock (${reason}).`);
1468
+ }
1469
+ let result;
1470
+ try {
1471
+ let pointer;
1472
+ try {
1473
+ pointer = await readSessionPointerForRecovery(context);
1474
+ }
1475
+ catch (error) {
1476
+ result = pointerRecoveryRefusal(context, 'io-error', `Unable to read the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`);
1477
+ return result;
1478
+ }
1479
+ if (pointer.status === 'absent') {
1480
+ result = pointerRecoveryRefusal(context, 'absent', 'No selected session pointer exists.');
1481
+ return result;
1482
+ }
1483
+ const sessionId = normalizeSessionId(pointer.state?.session_id);
1484
+ if (pointer.status !== 'stale-dead' || !pointer.state || !pointer.raw || !pointer.bytes || !sessionId) {
1485
+ const reason = pointer.status === 'usable'
1486
+ ? 'The selected session pointer owner is usable or live.'
1487
+ : pointer.status === 'identity-indeterminate'
1488
+ ? 'The selected session pointer owner identity is indeterminate.'
1489
+ : pointer.status === 'foreign-cwd'
1490
+ ? 'The selected session pointer belongs to a different working directory.'
1491
+ : 'The selected session pointer is malformed.';
1492
+ const status = pointer.status === 'stale-dead' ? 'malformed' : pointer.status;
1493
+ result = pointerRecoveryRefusal(context, status, reason, sessionId);
1494
+ return result;
1495
+ }
1496
+ const malformedReason = malformedRecoveryPointerReason(pointer.state);
1497
+ if (malformedReason) {
1498
+ result = pointerRecoveryRefusal(context, 'malformed', `The selected session pointer is malformed (${malformedReason}).`, sessionId);
1499
+ return result;
1500
+ }
1501
+ const authorityFailure = selectedRootAuthorityFailure(context, pointer.state);
1502
+ if (authorityFailure) {
1503
+ const reason = authorityFailure === 'foreign-cwd'
1504
+ ? 'The selected session pointer belongs to a different working-directory lineage.'
1505
+ : 'The selected session pointer does not have exact state_root authority for the selected root.';
1506
+ result = pointerRecoveryRefusal(context, authorityFailure, reason, sessionId);
1507
+ return result;
1508
+ }
1509
+ const recoveryLiveness = classifySessionProcessForRecovery(pointer.state);
1510
+ if (recoveryLiveness !== 'dead') {
1511
+ const status = recoveryLiveness === 'reused' ? 'reused' : recoveryLiveness;
1512
+ const reason = recoveryLiveness === 'reused'
1513
+ ? 'The selected session pointer PID was reused or its recorded birth identity mismatched.'
1514
+ : recoveryLiveness === 'usable'
1515
+ ? 'The selected session pointer owner is usable or live.'
1516
+ : 'The selected session pointer owner identity is indeterminate.';
1517
+ result = pointerRecoveryRefusal(context, status, reason, sessionId);
1518
+ return result;
1519
+ }
1520
+ let sourceStat;
1521
+ try {
1522
+ sourceStat = await transactionDependencies.fs.lstat(context.sessionPath);
1523
+ }
1524
+ catch (error) {
1525
+ result = pointerRecoveryRefusal(context, 'io-error', `Unable to inspect the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`, sessionId);
1526
+ return result;
1527
+ }
1528
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
1529
+ result = pointerRecoveryRefusal(context, 'malformed', 'The selected session pointer is not a regular file.', sessionId);
1530
+ return result;
1531
+ }
1532
+ const recoveryToken = transactionDependencies.token();
1533
+ if (!isValidToken(recoveryToken)) {
1534
+ result = pointerRecoveryRefusal(context, 'io-error', 'Unable to create a valid selected pointer quarantine token.', sessionId);
1535
+ return result;
1536
+ }
1537
+ const quarantinePath = `${context.sessionPath}.quarantine.${sessionId}.${recoveryToken}`;
1538
+ let quarantineMoveCommitted = false;
1539
+ try {
1540
+ const collision = await lstatRecoveryPath(quarantinePath);
1541
+ if (collision) {
1542
+ result = pointerRecoveryRefusal(context, 'collision', 'The selected pointer quarantine destination already exists.', sessionId);
1543
+ return result;
1544
+ }
1545
+ const currentStat = await transactionDependencies.fs.lstat(context.sessionPath);
1546
+ const currentBytes = await transactionDependencies.fs.readBytes(context.sessionPath);
1547
+ const currentAfterRead = await transactionDependencies.fs.lstat(context.sessionPath);
1548
+ const currentRaw = currentBytes.toString('utf8');
1549
+ let current;
1550
+ try {
1551
+ current = classifyParsedSessionPointerForRecovery(JSON.parse(currentRaw), currentRaw);
1552
+ }
1553
+ catch {
1554
+ result = pointerRecoveryRefusal(context, 'race', 'The selected session pointer changed before quarantine.', sessionId);
1555
+ return result;
1556
+ }
1557
+ if (!sameRecoveryIdentity(currentStat, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')
1558
+ || !sameRecoveryIdentity(currentAfterRead, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')
1559
+ || !currentBytes.equals(pointer.bytes)
1560
+ || current.status !== 'stale-dead'
1561
+ || !current.state
1562
+ || selectedRootAuthorityFailure(context, current.state) !== undefined
1563
+ || classifySessionProcessForRecovery(current.state) !== 'dead') {
1564
+ result = pointerRecoveryRefusal(context, 'race', 'The selected session pointer changed before quarantine.', sessionId);
1565
+ return result;
1566
+ }
1567
+ const moved = await moveRecoveryPathNoReplace(context.sessionPath, quarantinePath, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file', () => { quarantineMoveCommitted = true; });
1568
+ if (!moved.moved) {
1569
+ const destinationExists = await lstatRecoveryPath(quarantinePath);
1570
+ const sourceExists = await lstatRecoveryPath(context.sessionPath);
1571
+ const capturedDestination = destinationExists
1572
+ && sameRecoveryIdentity(destinationExists, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file');
1573
+ if (capturedDestination) {
1574
+ result = {
1575
+ status: 'recovery-required',
1576
+ pointerPath: context.sessionPath,
1577
+ action: 'quarantined',
1578
+ recovered: false,
1579
+ reason: sourceExists
1580
+ ? 'The atomic move outcome was ambiguous; the captured selected pointer is present at quarantine and a successor occupies the canonical path, so both were preserved for explicit recovery.'
1581
+ : 'The atomic move outcome was ambiguous, but the captured selected pointer is present at quarantine; forensic residue was preserved for explicit recovery.',
1582
+ sessionId,
1583
+ quarantinePath,
1584
+ };
1585
+ return result;
1586
+ }
1587
+ const foreignDestination = destinationExists
1588
+ && !capturedDestination;
1589
+ result = pointerRecoveryRefusal(context, moved.unsupported ? 'unsupported' : foreignDestination && !sourceExists ? 'race' : destinationExists ? 'collision' : 'race', moved.reason ?? 'The selected session pointer could not be quarantined atomically.', sessionId);
1590
+ return result;
1591
+ }
1592
+ const archivedStat = await lstatRecoveryPath(quarantinePath);
1593
+ const sourceAfterMove = await lstatRecoveryPath(context.sessionPath);
1594
+ const archivedBytes = archivedStat && sameRecoveryIdentity(archivedStat, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')
1595
+ ? await transactionDependencies.fs.readBytes(quarantinePath)
1596
+ : undefined;
1597
+ const archivedRaw = archivedBytes?.toString('utf8');
1598
+ let archivedPointer;
1599
+ if (archivedRaw !== undefined) {
1600
+ try {
1601
+ archivedPointer = classifyParsedSessionPointerForRecovery(JSON.parse(archivedRaw), archivedRaw);
1602
+ }
1603
+ catch { /* handled below */ }
1604
+ }
1605
+ if (sourceAfterMove
1606
+ || !archivedBytes?.equals(pointer.bytes)
1607
+ || archivedPointer?.status !== 'stale-dead'
1608
+ || !archivedPointer.state
1609
+ || selectedRootAuthorityFailure(context, archivedPointer.state) !== undefined
1610
+ || classifySessionProcessForRecovery(archivedPointer.state) !== 'dead') {
1611
+ if (!sourceAfterMove && archivedBytes?.equals(pointer.bytes) && archivedStat
1612
+ && sameRecoveryIdentity(archivedStat, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')) {
1613
+ const rollback = await moveRecoveryPathNoReplace(quarantinePath, context.sessionPath, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file');
1614
+ if (rollback.moved) {
1615
+ quarantineMoveCommitted = false;
1616
+ result = pointerRecoveryRefusal(context, 'race', 'The selected pointer became uncertain after quarantine and was restored without losing forensic bytes.', sessionId);
1617
+ return result;
1618
+ }
1619
+ }
1620
+ result = {
1621
+ status: 'recovery-required',
1622
+ pointerPath: context.sessionPath,
1623
+ action: 'quarantined',
1624
+ recovered: false,
1625
+ reason: 'The selected pointer changed or became uncertain after quarantine; forensic residue was preserved for explicit recovery.',
1626
+ sessionId,
1627
+ quarantinePath,
1628
+ };
1629
+ return result;
1630
+ }
1631
+ result = {
1632
+ status: 'recovered',
1633
+ pointerPath: context.sessionPath,
1634
+ action: 'quarantined',
1635
+ recovered: true,
1636
+ reason: 'Verified-dead selected session pointer quarantined with exact forensic bytes preserved.',
1637
+ sessionId,
680
1638
  quarantinePath,
681
1639
  };
1640
+ return result;
1641
+ }
1642
+ catch (error) {
1643
+ if (quarantineMoveCommitted) {
1644
+ result = {
1645
+ status: 'recovery-required',
1646
+ pointerPath: context.sessionPath,
1647
+ action: 'quarantined',
1648
+ recovered: false,
1649
+ reason: `The selected pointer was quarantined, but post-move verification failed (${errorCode(error) ?? errorMessage(error)}); forensic residue was preserved for explicit recovery.`,
1650
+ sessionId,
1651
+ quarantinePath,
1652
+ };
1653
+ return result;
1654
+ }
1655
+ result = pointerRecoveryRefusal(context, 'io-error', `Unable to quarantine the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`, sessionId);
1656
+ return result;
1657
+ }
1658
+ }
1659
+ finally {
1660
+ const failures = await releasePointerLock(lock);
1661
+ if (failures.length > 0 && result) {
1662
+ result.status = 'recovery-required';
1663
+ result.recovered = false;
1664
+ result.reason = `${result.reason} Pointer lock release requires explicit lock recovery.`;
682
1665
  }
683
1666
  }
684
- return { ...inspection, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock was atomically claimed and quarantined.', quarantinePath };
685
1667
  }
686
1668
  function buildLockOwner(token) {
687
- let identity;
1669
+ const runtimePlatform = transactionDependencies.runtimePlatform;
1670
+ let observation;
688
1671
  try {
689
- identity = transactionDependencies.readProcessIdentity(process.pid, process.platform);
1672
+ observation = transactionDependencies.observeProcess(process.pid, runtimePlatform);
690
1673
  }
691
1674
  catch {
692
1675
  // Publication remains valid with optional identity metadata omitted.
693
1676
  }
694
- return {
695
- version: 1,
1677
+ const identity = observation?.kind === 'identity' && isValidProcessIdentity(observation.identity)
1678
+ && observation.identity.platform === runtimePlatform
1679
+ ? observation.identity
1680
+ : undefined;
1681
+ const legacyStartTicks = identity?.platform === 'linux' && Number.isSafeInteger(Number(identity.birth))
1682
+ ? Number(identity.birth)
1683
+ : undefined;
1684
+ const legacyHash = identity?.cmdline_hash && SHA256_PATTERN.test(identity.cmdline_hash)
1685
+ ? identity.cmdline_hash
1686
+ : undefined;
1687
+ const common = {
696
1688
  token,
697
1689
  pid: process.pid,
698
- platform: process.platform,
699
- ...(identity?.status === 'matching' && isValidStartTicks(identity.startTicks)
700
- ? { pid_start_ticks: identity.startTicks }
701
- : {}),
702
- ...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
703
- ? { pid_cmdline_hash: identity.cmdlineHash }
704
- : {}),
1690
+ platform: runtimePlatform,
1691
+ ...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
1692
+ ...(legacyHash ? { pid_cmdline_hash: legacyHash } : {}),
705
1693
  created_at: new Date(transactionDependencies.nowMs()).toISOString(),
706
1694
  };
1695
+ return identity
1696
+ ? { version: 2, ...common, process_identity: identity }
1697
+ : { version: 1, ...common };
707
1698
  }
708
1699
  async function removeOwnedPath(path) {
709
1700
  try {
@@ -942,7 +1933,7 @@ async function releasePointerLock(lock) {
942
1933
  if (!isNotFound(error)) {
943
1934
  return [{
944
1935
  operation: 'lock-release',
945
- phase: 'remove-release-dir',
1936
+ phase: 'remove-release-owner',
946
1937
  ownership: 'released',
947
1938
  message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
948
1939
  cause: error,
@@ -965,6 +1956,10 @@ async function releasePointerLock(lock) {
965
1956
  }];
966
1957
  }
967
1958
  }
1959
+ /** @internal Test-only release of a held pointer lock; do not use outside session tests. */
1960
+ export async function __releasePointerLockForTests(cwd, token) {
1961
+ return await releasePointerLock({ context: resolveSessionPointerContext(cwd), token });
1962
+ }
968
1963
  function recoveryAbort(context, primary, failures, operation) {
969
1964
  return resolvedAbort(context, {
970
1965
  code: 'session_pointer_lock_recovery_required',
@@ -979,9 +1974,12 @@ function recoveryAbort(context, primary, failures, operation) {
979
1974
  secondaryFailures: failures,
980
1975
  });
981
1976
  }
982
- function releaseFailureError(failures) {
983
- const error = new Error('Session pointer committed, but lock release left recovery evidence.');
984
- Object.assign(error, { secondaryFailures: failures });
1977
+ function releaseFailureError(failures, context, value) {
1978
+ const error = new CommittedLaunchBlockedError(failures);
1979
+ if (context)
1980
+ error.context = context;
1981
+ if (value !== undefined)
1982
+ error.value = value;
985
1983
  return error;
986
1984
  }
987
1985
  async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
@@ -1023,7 +2021,7 @@ function ownerConflictAbort(context, candidateSessionId, state, cause) {
1023
2021
  ...(cause ? { cause } : {}),
1024
2022
  });
1025
2023
  }
1026
- async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
2024
+ async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState, beforeCommit, afterCommit) {
1027
2025
  let context;
1028
2026
  try {
1029
2027
  context = options.context ?? resolveSessionPointerContext(cwd);
@@ -1059,6 +2057,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1059
2057
  const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
1060
2058
  const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
1061
2059
  let pointerCommitted = false;
2060
+ let pointerTempWritten = false;
1062
2061
  try {
1063
2062
  let pointer;
1064
2063
  try {
@@ -1076,7 +2075,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1076
2075
  }
1077
2076
  let next;
1078
2077
  try {
1079
- next = transition(pointer, context);
2078
+ next = await transition(pointer, context);
1080
2079
  }
1081
2080
  catch (error) {
1082
2081
  if (isSessionPointerLaunchAbort(error))
@@ -1091,6 +2090,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1091
2090
  }
1092
2091
  const serialized = JSON.stringify(pointerState(next), null, 2);
1093
2092
  try {
2093
+ pointerTempWritten = true;
1094
2094
  await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
1095
2095
  }
1096
2096
  catch (error) {
@@ -1116,11 +2116,31 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1116
2116
  cause: error,
1117
2117
  });
1118
2118
  }
2119
+ if (beforeCommit)
2120
+ await beforeCommit(context);
1119
2121
  try {
1120
2122
  await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
1121
2123
  pointerCommitted = true;
2124
+ if (afterCommit) {
2125
+ try {
2126
+ await afterCommit(context);
2127
+ }
2128
+ catch (error) {
2129
+ const releaseFailures = await releasePointerLock(lock);
2130
+ throw releaseFailureError([
2131
+ {
2132
+ operation: 'lock-release', phase: 'rename', ownership: 'uncertain',
2133
+ message: `Selected state root identity changed after pointer publication: ${errorMessage(error)}`,
2134
+ cause: error, evidencePath: context.sessionPath,
2135
+ },
2136
+ ...releaseFailures,
2137
+ ], context, next);
2138
+ }
2139
+ }
1122
2140
  }
1123
2141
  catch (error) {
2142
+ if (error instanceof CommittedLaunchBlockedError)
2143
+ throw error;
1124
2144
  throw resolvedAbort(context, {
1125
2145
  code: 'session_pointer_io_failure',
1126
2146
  operation: 'pointer-rename',
@@ -1132,7 +2152,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1132
2152
  }
1133
2153
  const releaseFailures = await releasePointerLock(lock);
1134
2154
  if (releaseFailures.length > 0)
1135
- throw releaseFailureError(releaseFailures);
2155
+ throw releaseFailureError(releaseFailures, context, next);
1136
2156
  emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
1137
2157
  return { context, value: next };
1138
2158
  }
@@ -1149,21 +2169,44 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1149
2169
  reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
1150
2170
  cause: error,
1151
2171
  });
1152
- const ownsPointerTemp = primary.operation === 'pointer-temp-write'
1153
- || primary.operation === 'pointer-fsync'
1154
- || primary.operation === 'pointer-rename';
2172
+ const ownsPointerTemp = pointerTempWritten;
1155
2173
  throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
1156
2174
  }
1157
2175
  }
1158
- function startPointerTransition(requestedSessionId, options) {
2176
+ function startPointerTransition(requestedSessionId, options, requireLaunchLineageToken = false, requireAbsent = false) {
1159
2177
  return (pointer, context) => {
1160
- if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
2178
+ if (requireAbsent && pointer.status !== 'absent') {
2179
+ // Same read-vs-authority split: an occupied pointer is an owner conflict even when its
2180
+ // liveness is indeterminate, which is the only classification a live pointer can get on a
2181
+ // platform without process-birth evidence.
2182
+ if (pointer.state && (pointer.status === 'usable' || isUnobservableLivePointerState(pointer.state))) {
2183
+ throw ownerConflictAbort(context, requestedSessionId, pointer.state);
2184
+ }
2185
+ throw unusablePointerAbort(context, requestedSessionId, pointer);
2186
+ }
2187
+ // Read semantics, not authority: `identity-indeterminate` means "someone may still be here",
2188
+ // and on platforms that record no process-birth evidence EVERY live pointer classifies that way
2189
+ // (see probeIdentitylessProcess). Rejecting it outright refused legitimate sessions with a
2190
+ // generic unusable-pointer abort and skipped lineage/metadata preservation. It is admitted as
2191
+ // existing evidence; the ownership/compatibility checks below still refuse any pointer that does
2192
+ // not match, so a foreign claim gets the exact owner-conflict abort. Genuinely unusable evidence
2193
+ // (malformed, foreign-cwd) is still refused, and authority to own writes still requires 'usable'.
2194
+ if (pointer.status !== 'absent'
2195
+ && pointer.status !== 'stale-dead'
2196
+ && pointer.status !== 'usable'
2197
+ && !(pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))) {
1161
2198
  throw unusablePointerAbort(context, requestedSessionId, pointer);
1162
2199
  }
1163
- const existing = pointer.status === 'usable' ? pointer.state : undefined;
2200
+ const existing = pointer.status === 'usable'
2201
+ || (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
2202
+ ? pointer.state
2203
+ : undefined;
1164
2204
  if (existing && !isStartCompatible(existing, requestedSessionId)) {
1165
2205
  throw ownerConflictAbort(context, requestedSessionId, existing);
1166
2206
  }
2207
+ if (requireLaunchLineageToken && existing && !isValidToken(existing.launch_lineage_token)) {
2208
+ throw ownerConflictAbort(context, requestedSessionId, existing);
2209
+ }
1167
2210
  const canonicalSessionId = existing?.session_id ?? requestedSessionId;
1168
2211
  const pid = resolvePid(options);
1169
2212
  const platform = options.platform ?? process.platform;
@@ -1180,20 +2223,51 @@ function startPointerTransition(requestedSessionId, options) {
1180
2223
  pid,
1181
2224
  }, error);
1182
2225
  }
1183
- return createSessionState(context.cwd, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
2226
+ const state = createSessionState(context.cwd, context.baseStateDir, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
1184
2227
  nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
1185
2228
  previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
1186
2229
  nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
1187
2230
  ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
2231
+ launchLineageToken: existing
2232
+ ? (isValidToken(existing.launch_lineage_token) ? existing.launch_lineage_token : undefined)
2233
+ : transactionDependencies.token(),
1188
2234
  tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
1189
2235
  tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
1190
2236
  });
2237
+ return preserveExistingLaunchLineageToken(existing, state);
1191
2238
  };
1192
2239
  }
1193
- /** Write or merge a wrapper-owned canonical pointer through the exact-root transaction. */
2240
+ /** Create a wrapper-owned canonical pointer only when the selected pointer is absent. */
1194
2241
  export async function writeSessionStart(cwd, sessionId, options = {}) {
2242
+ const candidateSessionId = normalizeSessionId(sessionId);
2243
+ let context;
2244
+ try {
2245
+ context = options.context ?? resolveSessionPointerContext(cwd);
2246
+ }
2247
+ catch (error) {
2248
+ throw contextAbort(cwd, candidateSessionId, error);
2249
+ }
2250
+ let pointer;
2251
+ try {
2252
+ pointer = await readSessionPointer(context);
2253
+ }
2254
+ catch (error) {
2255
+ throw resolvedAbort(context, {
2256
+ code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
2257
+ lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
2258
+ });
2259
+ }
2260
+ if (pointer.status !== 'absent') {
2261
+ if (pointer.status === 'usable' && pointer.state) {
2262
+ throw ownerConflictAbort(context, candidateSessionId ?? sessionId, pointer.state);
2263
+ }
2264
+ throw unusablePointerAbort(context, candidateSessionId ?? sessionId, pointer);
2265
+ }
2266
+ return await writeSessionStartTransition(cwd, sessionId, { ...options, context });
2267
+ }
2268
+ async function writeSessionStartTransition(cwd, sessionId, options, requireLaunchLineageToken = false, requireAbsent = false, beforeCommit, afterCommit) {
1195
2269
  const requestedSessionId = normalizeSessionId(sessionId);
1196
- const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options), (state) => state);
2270
+ const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options, requireLaunchLineageToken, requireAbsent), (state) => state, beforeCommit, afterCommit);
1197
2271
  await appendToLogAtContext(result.context, {
1198
2272
  event: 'session_start',
1199
2273
  session_id: result.value.session_id,
@@ -1203,51 +2277,613 @@ export async function writeSessionStart(cwd, sessionId, options = {}) {
1203
2277
  }).catch(() => { });
1204
2278
  return result.value;
1205
2279
  }
2280
+ const DIRECTORY_CAPABILITY_PLATFORMS = new Set([
2281
+ 'linux', 'darwin', 'freebsd', 'openbsd', 'netbsd', 'sunos',
2282
+ ]);
2283
+ export class LaunchContextResolutionError extends Error {
2284
+ cleanup;
2285
+ name = 'LaunchContextResolutionError';
2286
+ constructor(cleanup, cause) {
2287
+ super(`Unable to establish launch directory capability: ${errorMessage(cause)}`, { cause });
2288
+ this.cleanup = cleanup;
2289
+ }
2290
+ }
2291
+ const bindingLeases = new WeakMap();
2292
+ const bindingFinalizations = new WeakMap();
2293
+ function safeError(error) {
2294
+ return {
2295
+ name: error instanceof Error ? error.name : 'Error',
2296
+ message: errorMessage(error),
2297
+ ...(errorCode(error) ? { code: errorCode(error) } : {}),
2298
+ };
2299
+ }
2300
+ function lockDisposition(failures) {
2301
+ if (failures.some((failure) => failure.ownership === 'uncertain'))
2302
+ return 'uncertain';
2303
+ return failures.some((failure) => failure.ownership === 'held') ? 'held' : 'released-with-residue';
2304
+ }
2305
+ async function acquireDirectoryCapability(cwd, platform) {
2306
+ const cleanup = [];
2307
+ let canonicalRealpath;
2308
+ try {
2309
+ canonicalRealpath = await realpath(cwd);
2310
+ }
2311
+ catch (error) {
2312
+ throw new LaunchContextResolutionError({ capability: cleanup }, error);
2313
+ }
2314
+ if (!DIRECTORY_CAPABILITY_PLATFORMS.has(platform)) {
2315
+ return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'platform' }, lease: { closed: false }, cleanup };
2316
+ }
2317
+ let handle;
2318
+ try {
2319
+ handle = await open(canonicalRealpath, 'r');
2320
+ }
2321
+ catch (error) {
2322
+ throw new LaunchContextResolutionError({ capability: cleanup }, error);
2323
+ }
2324
+ let statFailure;
2325
+ try {
2326
+ const stats = await handle.stat({ bigint: true });
2327
+ if (!stats.isDirectory() || typeof stats.dev !== 'bigint' || typeof stats.ino !== 'bigint' || stats.dev <= 0n || stats.ino <= 0n) {
2328
+ try {
2329
+ await handle.close();
2330
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
2331
+ }
2332
+ catch (closeError) {
2333
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
2334
+ throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
2335
+ }
2336
+ return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'inadequate-identity' }, lease: { closed: false }, cleanup };
2337
+ }
2338
+ return { canonicalRealpath, identity: { kind: 'supported', dev: stats.dev, ino: stats.ino }, lease: { handle, closed: false }, cleanup };
2339
+ }
2340
+ catch (error) {
2341
+ if (error instanceof LaunchContextResolutionError)
2342
+ throw error;
2343
+ statFailure = error;
2344
+ }
2345
+ try {
2346
+ await handle.close();
2347
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
2348
+ }
2349
+ catch (closeError) {
2350
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
2351
+ throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
2352
+ }
2353
+ const code = errorCode(statFailure);
2354
+ if (code === 'ENOSYS' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EINVAL') {
2355
+ return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'stat-feature' }, lease: { closed: false }, cleanup };
2356
+ }
2357
+ throw new LaunchContextResolutionError({ capability: cleanup }, statFailure);
2358
+ }
2359
+ export async function closeLaunchSessionBindingOnce(binding, phase = 'post-finalization') {
2360
+ const lease = bindingLeases.get(binding);
2361
+ if (!lease || lease.closed)
2362
+ return lease?.closeEvidence ?? { role: 'original-retained', phase, status: 'not-needed' };
2363
+ lease.closed = true;
2364
+ try {
2365
+ await lease.handle?.close();
2366
+ return lease.closeEvidence = { role: 'original-retained', phase, status: 'closed' };
2367
+ }
2368
+ catch (error) {
2369
+ return lease.closeEvidence = { role: 'original-retained', phase, status: 'failed', error: safeError(error) };
2370
+ }
2371
+ }
2372
+ export async function establishLaunchSessionBinding(cwd, requestedSessionId, options = {}) {
2373
+ const platform = options.platform ?? process.platform;
2374
+ let context;
2375
+ try {
2376
+ context = options.context ?? resolveSessionPointerContext(cwd);
2377
+ await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
2378
+ }
2379
+ catch (error) {
2380
+ throw new LaunchContextResolutionError({ capability: [] }, error);
2381
+ }
2382
+ const acquired = await acquireDirectoryCapability(context.baseStateDir, platform);
2383
+ const cleanup = () => ({ capability: acquired.cleanup });
2384
+ const revalidateSelectedRoot = async (lockedContext) => {
2385
+ if (acquired.identity.kind !== 'supported')
2386
+ return;
2387
+ const retained = await acquired.lease.handle?.stat({ bigint: true });
2388
+ if (!retained || !retained.isDirectory() || retained.dev !== acquired.identity.dev || retained.ino !== acquired.identity.ino) {
2389
+ throw resolvedAbort(lockedContext, {
2390
+ code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
2391
+ lockPath: lockedContext.lockPath, reason: 'Selected state root identity changed during pointer publication.',
2392
+ });
2393
+ }
2394
+ const canonical = await realpath(lockedContext.baseStateDir);
2395
+ const fresh = await open(canonical, 'r');
2396
+ try {
2397
+ const stats = await fresh.stat({ bigint: true });
2398
+ if (canonical !== acquired.canonicalRealpath || !stats.isDirectory() || stats.dev !== acquired.identity.dev || stats.ino !== acquired.identity.ino) {
2399
+ throw resolvedAbort(lockedContext, {
2400
+ code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
2401
+ lockPath: lockedContext.lockPath, reason: 'Selected state root was replaced during pointer publication.',
2402
+ });
2403
+ }
2404
+ }
2405
+ finally {
2406
+ await fresh.close();
2407
+ }
2408
+ };
2409
+ try {
2410
+ const state = await writeSessionStartTransition(context.cwd, requestedSessionId, { ...options, context }, true, true, revalidateSelectedRoot, revalidateSelectedRoot);
2411
+ const token = state.launch_lineage_token;
2412
+ if (!isValidToken(token)) {
2413
+ const close = await closeLease(acquired.lease, 'before-authorization');
2414
+ if (close)
2415
+ acquired.cleanup.push(close);
2416
+ throw new LaunchContextResolutionError(cleanup(), new Error('Committed session pointer lacks a valid lineage token.'));
2417
+ }
2418
+ const binding = Object.freeze({
2419
+ context: Object.freeze({ ...context }), canonicalRealpath: acquired.canonicalRealpath,
2420
+ directoryIdentity: acquired.identity, canonicalSessionId: state.session_id,
2421
+ ...(state.owner_omx_session_id ? { ownerOmxSessionId: state.owner_omx_session_id } : {}),
2422
+ ...(state.native_session_id ? { nativeSessionId: state.native_session_id } : {}),
2423
+ startedAt: state.started_at, launchLineageToken: token,
2424
+ });
2425
+ bindingLeases.set(binding, acquired.lease);
2426
+ return { kind: 'committed-released', binding, cleanup: cleanup() };
2427
+ }
2428
+ catch (error) {
2429
+ if (error instanceof CommittedLaunchBlockedError) {
2430
+ const state = error.value;
2431
+ const errorContext = error.context ?? context;
2432
+ const close = await closeLease(acquired.lease, 'before-authorization');
2433
+ if (close)
2434
+ acquired.cleanup.push(close);
2435
+ return {
2436
+ kind: 'committed-release-failed', evidence: { context: errorContext, canonicalSessionId: state?.session_id ?? requestedSessionId },
2437
+ error, lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup: cleanup(),
2438
+ };
2439
+ }
2440
+ const close = await closeLease(acquired.lease, 'before-authorization');
2441
+ if (close)
2442
+ acquired.cleanup.push(close);
2443
+ if (isSessionPointerLaunchAbort(error))
2444
+ return { kind: 'precommit-aborted', abort: error, cleanup: cleanup() };
2445
+ throw error;
2446
+ }
2447
+ }
2448
+ async function closeLease(lease, phase) {
2449
+ if (lease.closed)
2450
+ return undefined;
2451
+ lease.closed = true;
2452
+ try {
2453
+ await lease.handle?.close();
2454
+ return { role: 'acquisition', phase, status: 'closed' };
2455
+ }
2456
+ catch (error) {
2457
+ return { role: 'acquisition', phase, status: 'failed', error: safeError(error) };
2458
+ }
2459
+ }
2460
+ export async function updateDetachedSessionMetadata(binding, patch) {
2461
+ const capability = [];
2462
+ const cleanup = { capability };
2463
+ try {
2464
+ let pointerBeforeTransaction;
2465
+ try {
2466
+ pointerBeforeTransaction = await readSessionPointer(binding.context);
2467
+ }
2468
+ catch (error) {
2469
+ throw resolvedAbort(binding.context, {
2470
+ code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId: binding.canonicalSessionId,
2471
+ lockPath: binding.context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
2472
+ });
2473
+ }
2474
+ // Read against an ALREADY-authorized binding: isAuthorizedBoundPointer verifies the pointer
2475
+ // belongs to this binding (canonical session id + launch lineage token), which is a stricter
2476
+ // check than liveness. Admitting an unobservable-live pointer here only stops a running session
2477
+ // on a platform without process-birth evidence from reading as absent; it grants no new
2478
+ // authority, because the binding authorization still has to match.
2479
+ if ((pointerBeforeTransaction.status !== 'usable'
2480
+ && !(pointerBeforeTransaction.status === 'identity-indeterminate'
2481
+ && isUnobservableLivePointerState(pointerBeforeTransaction.state)))
2482
+ || !isAuthorizedBoundPointer(binding, binding.context, binding.canonicalSessionId, pointerBeforeTransaction.state)) {
2483
+ throw unusablePointerAbort(binding.context, binding.canonicalSessionId, pointerBeforeTransaction);
2484
+ }
2485
+ const authorization = await authorizeBoundDirectoryBeforeTransaction(binding, pointerBeforeTransaction, binding.context.cwd);
2486
+ capability.push(...authorization.capability);
2487
+ const result = await writePointerTransaction(binding.context.cwd, binding.canonicalSessionId, { context: binding.context }, DEFAULT_POINTER_TIMEOUT_MS, async (pointer, context) => {
2488
+ const state = pointer.status === 'usable'
2489
+ || (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
2490
+ ? pointer.state
2491
+ : undefined;
2492
+ if (!state || !isAuthorizedBoundPointer(binding, context, binding.canonicalSessionId, state)) {
2493
+ throw unusablePointerAbort(context, binding.canonicalSessionId, pointer);
2494
+ }
2495
+ await consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer);
2496
+ return {
2497
+ ...state,
2498
+ ...(patch.tmuxSessionName !== undefined ? { tmux_session_name: patch.tmuxSessionName } : {}),
2499
+ ...(patch.tmuxPaneId !== undefined ? { tmux_pane_id: patch.tmuxPaneId } : {}),
2500
+ };
2501
+ }, (state) => state);
2502
+ return { kind: 'committed-released', evidence: { context: result.context, canonicalSessionId: binding.canonicalSessionId }, cleanup };
2503
+ }
2504
+ catch (error) {
2505
+ if (error instanceof CommittedLaunchBlockedError) {
2506
+ const context = error.context ?? binding.context;
2507
+ return { kind: 'committed-release-failed', evidence: { context, canonicalSessionId: binding.canonicalSessionId }, error,
2508
+ lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup };
2509
+ }
2510
+ if (isSessionPointerLaunchAbort(error))
2511
+ return { kind: 'precommit-aborted', abort: error, cleanup };
2512
+ throw error;
2513
+ }
2514
+ }
2515
+ function isAuthorizedBoundPointer(binding, context, candidateSessionId, state) {
2516
+ traceSessionFinalizationOperation('auth-function-enter');
2517
+ traceSessionFinalizationOperation('lease-lookup-start');
2518
+ const lease = bindingLeases.get(binding);
2519
+ traceSessionFinalizationOperation('lease-lookup-done');
2520
+ traceSessionFinalizationOperation(`state-present:${state ? 'true' : 'false'}`);
2521
+ if (!lease || (binding.directoryIdentity.kind === 'supported' && lease.closed) || !state) {
2522
+ traceSessionFinalizationOperation('bound-auth-fail:lease-or-state');
2523
+ return false;
2524
+ }
2525
+ if (!isValidToken(binding.launchLineageToken) || !isValidToken(state.launch_lineage_token)) {
2526
+ traceSessionFinalizationOperation('bound-auth-fail:token-shape');
2527
+ return false;
2528
+ }
2529
+ if (candidateSessionId !== binding.canonicalSessionId) {
2530
+ traceSessionFinalizationOperation('bound-auth-fail:candidate-session');
2531
+ return false;
2532
+ }
2533
+ if (state.launch_lineage_token !== binding.launchLineageToken) {
2534
+ traceSessionFinalizationOperation('bound-auth-fail:lineage-token');
2535
+ return false;
2536
+ }
2537
+ if (state.started_at !== binding.startedAt) {
2538
+ traceSessionFinalizationOperation('bound-auth-fail:started-at');
2539
+ return false;
2540
+ }
2541
+ const directCanonicalIdentity = state.session_id === binding.canonicalSessionId
2542
+ && ((state.owner_omx_session_id ?? undefined) === binding.ownerOmxSessionId
2543
+ || (binding.ownerOmxSessionId === undefined && state.owner_omx_session_id === binding.canonicalSessionId))
2544
+ && (binding.nativeSessionId === undefined || state.native_session_id === binding.nativeSessionId);
2545
+ const replacedNativeIdentity = state.owner_omx_session_id === binding.canonicalSessionId
2546
+ && normalizeSessionId(state.session_id) !== undefined
2547
+ && state.native_session_id === state.session_id;
2548
+ if (!directCanonicalIdentity && !replacedNativeIdentity) {
2549
+ traceSessionFinalizationOperation('bound-auth-fail:session-identity');
2550
+ return false;
2551
+ }
2552
+ if (context.rootSource !== binding.context.rootSource)
2553
+ traceSessionFinalizationOperation('bound-auth-fail:root-source');
2554
+ else if (context.baseStateDir !== binding.context.baseStateDir)
2555
+ traceSessionFinalizationOperation('bound-auth-fail:base-state-dir');
2556
+ else if (context.sessionPath !== binding.context.sessionPath)
2557
+ traceSessionFinalizationOperation('bound-auth-fail:session-path');
2558
+ else if (context.cwd !== binding.context.cwd)
2559
+ traceSessionFinalizationOperation('bound-auth-fail:context-cwd');
2560
+ else if (state.cwd !== binding.context.cwd)
2561
+ traceSessionFinalizationOperation('bound-auth-fail:state-cwd');
2562
+ else {
2563
+ traceSessionFinalizationOperation('bound-auth-success');
2564
+ return true;
2565
+ }
2566
+ return false;
2567
+ }
2568
+ function isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, state) {
2569
+ return isAuthorizedBoundPointer(binding, context, candidateSessionId, state)
2570
+ && Boolean(normalizeSessionId(state?.native_session_id));
2571
+ }
2572
+ class BoundDirectoryComparisonDenied extends Error {
2573
+ comparison;
2574
+ capability;
2575
+ constructor(comparison, capability) {
2576
+ super(comparison.reason);
2577
+ this.comparison = comparison;
2578
+ this.capability = capability;
2579
+ }
2580
+ }
2581
+ function isAuthorizedBoundPointerForFinalization(binding, context, candidateSessionId, pointer, phase) {
2582
+ traceSessionFinalizationOperation(`${phase}-status:${pointer.status}`);
2583
+ if (pointer.status === 'usable') {
2584
+ traceSessionFinalizationOperation(`${phase}-usable-branch-enter`);
2585
+ return isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
2586
+ }
2587
+ if (pointer.status === 'stale-dead') {
2588
+ traceSessionFinalizationOperation(`${phase}-stale-dead-branch-enter`);
2589
+ return isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, pointer.state);
2590
+ }
2591
+ if (pointer.status === 'identity-indeterminate') {
2592
+ traceSessionFinalizationOperation(`${phase}-identity-indeterminate-branch-enter`);
2593
+ const sameProcess = pointer.state?.pid === process.pid;
2594
+ traceSessionFinalizationOperation(`${phase}-identity-indeterminate-current-pid:${sameProcess ? 'true' : 'false'}`);
2595
+ return sameProcess && isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
2596
+ }
2597
+ traceSessionFinalizationOperation(`${phase}-unsupported-status`);
2598
+ return false;
2599
+ }
2600
+ async function authorizeBoundDirectoryBeforeTransaction(binding, pointer, postLaunchCwd) {
2601
+ const capability = [];
2602
+ const lease = bindingLeases.get(binding);
2603
+ if (!lease || lease.closed) {
2604
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'binding-lease-closed' }, capability);
2605
+ }
2606
+ try {
2607
+ if (!sameFilePath(binding.context.cwd, postLaunchCwd)
2608
+ || !pointer.state?.cwd
2609
+ || !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
2610
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch' }, capability);
2611
+ }
2612
+ if (binding.directoryIdentity.kind !== 'supported') {
2613
+ return {
2614
+ comparison: { status: 'matched', reason: `unsupported-directory-capability:${binding.directoryIdentity.reason}` },
2615
+ capability,
2616
+ pointerStatus: pointer.status,
2617
+ pointerRaw: pointer.raw,
2618
+ };
2619
+ }
2620
+ const retained = fstatSync(lease.handle.fd, { bigint: true });
2621
+ if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
2622
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, capability);
2623
+ }
2624
+ const canonical = await realpath(binding.context.baseStateDir);
2625
+ const fd = openSync(canonical, 'r');
2626
+ let matched = false;
2627
+ try {
2628
+ const stats = fstatSync(fd, { bigint: true });
2629
+ matched = canonical === binding.canonicalRealpath
2630
+ && stats.isDirectory()
2631
+ && stats.dev === binding.directoryIdentity.dev
2632
+ && stats.ino === binding.directoryIdentity.ino;
2633
+ }
2634
+ finally {
2635
+ try {
2636
+ closeSync(fd);
2637
+ capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
2638
+ }
2639
+ catch (error) {
2640
+ capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
2641
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed' }, capability);
2642
+ }
2643
+ }
2644
+ if (!matched)
2645
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch' }, capability);
2646
+ return { comparison: { status: 'matched' }, capability, pointerStatus: pointer.status, pointerRaw: pointer.raw };
2647
+ }
2648
+ catch (error) {
2649
+ if (error instanceof BoundDirectoryComparisonDenied)
2650
+ throw error;
2651
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, capability);
2652
+ }
2653
+ }
2654
+ async function consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer) {
2655
+ if (authorization.comparison?.status !== 'matched' || binding.directoryIdentity.kind !== 'supported')
2656
+ return authorization;
2657
+ const lease = bindingLeases.get(binding);
2658
+ try {
2659
+ const retained = lease?.handle ? fstatSync(lease.handle.fd, { bigint: true }) : undefined;
2660
+ if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
2661
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, authorization.capability);
2662
+ }
2663
+ if (!pointer.state?.cwd || !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
2664
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch-under-lock' }, authorization.capability);
2665
+ }
2666
+ const canonical = await realpath(binding.context.baseStateDir);
2667
+ const fd = openSync(canonical, 'r');
2668
+ let matched = false;
2669
+ try {
2670
+ const stats = fstatSync(fd, { bigint: true });
2671
+ matched = canonical === binding.canonicalRealpath
2672
+ && stats.isDirectory()
2673
+ && stats.dev === binding.directoryIdentity.dev
2674
+ && stats.ino === binding.directoryIdentity.ino;
2675
+ }
2676
+ finally {
2677
+ try {
2678
+ closeSync(fd);
2679
+ authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
2680
+ }
2681
+ catch (error) {
2682
+ authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
2683
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed-under-lock' }, authorization.capability);
2684
+ }
2685
+ }
2686
+ if (!matched)
2687
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch-under-lock' }, authorization.capability);
2688
+ if (pointer.status !== authorization.pointerStatus || pointer.raw !== authorization.pointerRaw) {
2689
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'pointer-changed-before-consume' }, authorization.capability);
2690
+ }
2691
+ return authorization;
2692
+ }
2693
+ catch (error) {
2694
+ if (error instanceof BoundDirectoryComparisonDenied)
2695
+ throw error;
2696
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, authorization.capability);
2697
+ }
2698
+ }
2699
+ export function finalizeBoundOnce(binding, _reason, postLaunchCwd = binding.context.cwd) {
2700
+ const existing = bindingFinalizations.get(binding);
2701
+ if (existing)
2702
+ return existing;
2703
+ const finalization = (async () => {
2704
+ try {
2705
+ const revalidation = await writeSessionEnd(binding.context.cwd, binding.canonicalSessionId, {
2706
+ context: binding.context, binding, postLaunchCwd,
2707
+ });
2708
+ return { finalized: true, cleanup: { capability: revalidation.capability, comparison: revalidation.comparison } };
2709
+ }
2710
+ catch (error) {
2711
+ if (error instanceof BoundDirectoryComparisonDenied) {
2712
+ return { finalized: false, cleanup: { capability: error.capability, comparison: error.comparison } };
2713
+ }
2714
+ if (isSessionPointerLaunchAbort(error)) {
2715
+ return { finalized: false, cleanup: { capability: [], comparison: { status: 'denied', reason: error.code } } };
2716
+ }
2717
+ throw error;
2718
+ }
2719
+ })();
2720
+ bindingFinalizations.set(binding, finalization);
2721
+ return finalization;
2722
+ }
2723
+ function nativeSessionOwnerTransition(nativeSessionId, options) {
2724
+ return (pointer, context) => {
2725
+ // Read semantics, not authority: `identity-indeterminate` means "someone may still be here",
2726
+ // and on platforms that record no process-birth evidence EVERY live pointer classifies that way
2727
+ // (see probeIdentitylessProcess). Rejecting it outright refused legitimate sessions with a
2728
+ // generic unusable-pointer abort and skipped lineage/metadata preservation. It is admitted as
2729
+ // existing evidence; the ownership/compatibility checks below still refuse any pointer that does
2730
+ // not match, so a foreign claim gets the exact owner-conflict abort. Genuinely unusable evidence
2731
+ // (malformed, foreign-cwd) is still refused, and authority to own writes still requires 'usable'.
2732
+ if (pointer.status !== 'absent'
2733
+ && pointer.status !== 'stale-dead'
2734
+ && pointer.status !== 'usable'
2735
+ && !(pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))) {
2736
+ throw unusablePointerAbort(context, nativeSessionId, pointer);
2737
+ }
2738
+ const pid = resolvePid(options);
2739
+ const platform = options.platform ?? process.platform;
2740
+ const linuxIdentity = sessionIdentityFor(pid, platform);
2741
+ const existing = pointer.status === 'usable'
2742
+ || (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
2743
+ ? pointer.state
2744
+ : undefined;
2745
+ if (existing && (normalizeSessionId(existing.session_id) !== nativeSessionId
2746
+ || normalizeSessionId(existing.native_session_id) !== nativeSessionId
2747
+ || existing.platform !== platform
2748
+ || !isSessionStateAuthoritativeForCwd(existing, context.cwd)
2749
+ || existing.pid !== pid)) {
2750
+ throw ownerConflictAbort(context, nativeSessionId, existing);
2751
+ }
2752
+ return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
2753
+ nativeSessionId,
2754
+ startedAt: existing?.started_at,
2755
+ tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
2756
+ tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
2757
+ });
2758
+ };
2759
+ }
2760
+ export async function writeNativeSessionOwner(cwd, nativeSessionId, options = {}) {
2761
+ const normalized = normalizeSessionId(nativeSessionId);
2762
+ const context = resolveNativeSessionOwnerContext(cwd, nativeSessionId);
2763
+ const result = await writePointerTransaction(cwd, normalized, { context }, NATIVE_POINTER_TIMEOUT_MS, nativeSessionOwnerTransition(normalized ?? nativeSessionId, options), (state) => state);
2764
+ return result.value;
2765
+ }
2766
+ /** Read and classify the exact native session-owner sidecar without consulting alternate roots. */
2767
+ export async function readNativeSessionOwnerEvidence(cwd, nativeSessionId) {
2768
+ const normalized = normalizeSessionId(nativeSessionId);
2769
+ if (!normalized)
2770
+ return { status: 'malformed' };
2771
+ const context = resolveNativeSessionOwnerContext(cwd, normalized);
2772
+ const nativeOwnerDir = dirname(context.sessionPath);
2773
+ const sessionsDir = dirname(nativeOwnerDir);
2774
+ const stateRootDir = dirname(sessionsDir);
2775
+ const directoryPaths = [stateRootDir, sessionsDir, nativeOwnerDir];
2776
+ const directoryIdentities = [];
2777
+ try {
2778
+ for (const directoryPath of directoryPaths) {
2779
+ const stat = await transactionDependencies.fs.lstat(directoryPath);
2780
+ if (stat.isSymbolicLink() || !stat.isDirectory())
2781
+ return { status: 'malformed' };
2782
+ directoryIdentities.push({ path: directoryPath, dev: stat.dev, ino: stat.ino });
2783
+ }
2784
+ const pathStat = await transactionDependencies.fs.lstat(context.sessionPath);
2785
+ if (pathStat.isSymbolicLink() || !pathStat.isFile())
2786
+ return { status: 'malformed' };
2787
+ }
2788
+ catch (error) {
2789
+ if (isNotFound(error))
2790
+ return { status: 'absent' };
2791
+ throw error;
2792
+ }
2793
+ const handle = await open(context.sessionPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
2794
+ let raw;
2795
+ try {
2796
+ const openedStat = await handle.stat();
2797
+ if (!openedStat.isFile())
2798
+ return { status: 'malformed' };
2799
+ raw = await handle.readFile({ encoding: 'utf8' });
2800
+ const finalPathStat = await transactionDependencies.fs.lstat(context.sessionPath);
2801
+ if (finalPathStat.isSymbolicLink()
2802
+ || finalPathStat.dev !== openedStat.dev
2803
+ || finalPathStat.ino !== openedStat.ino) {
2804
+ return { status: 'malformed', raw };
2805
+ }
2806
+ for (const identity of directoryIdentities) {
2807
+ const finalDirectoryStat = await transactionDependencies.fs.lstat(identity.path);
2808
+ if (finalDirectoryStat.isSymbolicLink()
2809
+ || !finalDirectoryStat.isDirectory()
2810
+ || finalDirectoryStat.dev !== identity.dev
2811
+ || finalDirectoryStat.ino !== identity.ino) {
2812
+ return { status: 'malformed', raw };
2813
+ }
2814
+ }
2815
+ }
2816
+ finally {
2817
+ await handle.close();
2818
+ }
2819
+ let pointer;
2820
+ try {
2821
+ pointer = classifyParsedSessionPointer(context, JSON.parse(raw), raw);
2822
+ }
2823
+ catch (error) {
2824
+ if (error instanceof SyntaxError)
2825
+ return { status: 'malformed', raw };
2826
+ throw error;
2827
+ }
2828
+ if (pointer.status === 'absent' || pointer.status === 'foreign-cwd' || pointer.status === 'malformed' || pointer.status === 'identity-indeterminate') {
2829
+ return pointer;
2830
+ }
2831
+ const state = pointer.state;
2832
+ return state?.session_id === normalized
2833
+ && state.native_session_id === normalized
2834
+ && state.platform === transactionDependencies.runtimePlatform
2835
+ && isSessionStateAuthoritativeForCwd(state, context.cwd)
2836
+ ? pointer
2837
+ : { status: 'malformed', state, raw: pointer.raw };
2838
+ }
2839
+ export async function readNativeSessionOwner(cwd, nativeSessionId) {
2840
+ try {
2841
+ const evidence = await readNativeSessionOwnerEvidence(cwd, nativeSessionId);
2842
+ return evidence.status === 'usable' ? evidence.state ?? null : null;
2843
+ }
2844
+ catch {
2845
+ return null;
2846
+ }
2847
+ }
1206
2848
  function reconcileNativeTransition(nativeSessionId, options) {
1207
2849
  return (pointer, context) => {
1208
- if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
2850
+ // Read semantics, not authority: `identity-indeterminate` means "someone may still be here",
2851
+ // and on platforms that record no process-birth evidence EVERY live pointer classifies that way
2852
+ // (see probeIdentitylessProcess). Rejecting it outright refused legitimate sessions with a
2853
+ // generic unusable-pointer abort and skipped lineage/metadata preservation. It is admitted as
2854
+ // existing evidence; the ownership/compatibility checks below still refuse any pointer that does
2855
+ // not match, so a foreign claim gets the exact owner-conflict abort. Genuinely unusable evidence
2856
+ // (malformed, foreign-cwd) is still refused, and authority to own writes still requires 'usable'.
2857
+ if (pointer.status !== 'absent'
2858
+ && pointer.status !== 'stale-dead'
2859
+ && pointer.status !== 'usable'
2860
+ && !(pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))) {
1209
2861
  throw unusablePointerAbort(context, nativeSessionId, pointer);
1210
2862
  }
1211
2863
  const pid = resolvePid(options);
1212
2864
  const platform = options.platform ?? process.platform;
1213
2865
  const linuxIdentity = sessionIdentityFor(pid, platform);
1214
- const existing = pointer.status === 'usable' ? pointer.state : undefined;
1215
- const nowIso = new Date().toISOString();
2866
+ const livenessProven = pointer.status === 'usable';
2867
+ const existing = livenessProven
2868
+ || (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
2869
+ ? pointer.state
2870
+ : undefined;
2871
+ if (existing && !livenessProven && !mayAdoptUnprovenPointer(existing, context.cwd)) {
2872
+ throw ownerConflictAbort(context, nativeSessionId, existing);
2873
+ }
1216
2874
  if (!existing) {
1217
2875
  const ownerCandidate = verifiedOwnerCandidate(context, options);
1218
2876
  const ownerOmxSessionId = ownerCandidate;
1219
- return {
1220
- state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
1221
- nativeSessionId,
1222
- ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1223
- }),
1224
- };
2877
+ return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
2878
+ nativeSessionId,
2879
+ ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
2880
+ });
1225
2881
  }
1226
2882
  const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
1227
2883
  if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
1228
- const ownerOmxSessionId = getOmxLaunchSessionId(existing);
1229
- return {
1230
- state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
1231
- nativeSessionId,
1232
- ...(ownerOmxSessionId ? {
1233
- previousNativeSessionId: existingNativeSessionId,
1234
- nativeSessionSwitchedAt: nowIso,
1235
- ownerOmxSessionId,
1236
- } : {}),
1237
- }),
1238
- ...(ownerOmxSessionId ? {
1239
- replacementLog: {
1240
- event: 'native_session_replaced',
1241
- session_id: ownerOmxSessionId,
1242
- ...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
1243
- previous_native_session_id: existingNativeSessionId,
1244
- replaced_by_native_session_id: nativeSessionId,
1245
- pid,
1246
- timestamp: nowIso,
1247
- },
1248
- } : {}),
1249
- };
2884
+ throw ownerConflictAbort(context, nativeSessionId, existing);
1250
2885
  }
2886
+ const nowIso = new Date().toISOString();
1251
2887
  const ownerCandidate = verifiedOwnerCandidate(context, options);
1252
2888
  let ownerOmxSessionId;
1253
2889
  try {
@@ -1256,39 +2892,37 @@ function reconcileNativeTransition(nativeSessionId, options) {
1256
2892
  catch (error) {
1257
2893
  throw ownerConflictAbort(context, nativeSessionId, existing, error);
1258
2894
  }
1259
- return {
1260
- state: createSessionState(context.cwd, existing.session_id, pid, platform, linuxIdentity, {
1261
- nowIso,
1262
- nativeSessionId,
1263
- previousNativeSessionId: existing.previous_native_session_id,
1264
- nativeSessionSwitchedAt: existing.native_session_switched_at,
1265
- ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1266
- startedAt: existing.started_at,
1267
- tmuxSessionName: existing.tmux_session_name,
1268
- tmuxPaneId: existing.tmux_pane_id,
1269
- }),
1270
- };
2895
+ return preserveExistingLaunchLineageToken(existing, createSessionState(context.cwd, context.baseStateDir, existing.session_id, pid, platform, linuxIdentity, {
2896
+ nowIso,
2897
+ nativeSessionId,
2898
+ previousNativeSessionId: existing.previous_native_session_id,
2899
+ nativeSessionSwitchedAt: existing.native_session_switched_at,
2900
+ ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
2901
+ startedAt: existing.started_at,
2902
+ tmuxSessionName: existing.tmux_session_name,
2903
+ tmuxPaneId: existing.tmux_pane_id,
2904
+ launchLineageToken: isValidToken(existing.launch_lineage_token)
2905
+ ? existing.launch_lineage_token
2906
+ : undefined,
2907
+ }));
1271
2908
  };
1272
2909
  }
1273
2910
  /**
1274
- * Reconcile a native SessionStart without borrowing another root's pointer.
1275
- * A different native ID retains the existing OMX owner chain, but never binds a
1276
- * new owner alias during that replacement transition.
2911
+ * Reconcile native SessionStart only when the selected pointer is absent, stale,
2912
+ * or already belongs to that native session. A different live native ID is
2913
+ * authoritative owner evidence and must never be replaced.
1277
2914
  */
1278
2915
  export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
1279
2916
  const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
1280
- const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (transition) => transition.state);
1281
- if (result.value.replacementLog) {
1282
- await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
1283
- }
2917
+ const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (state) => state);
1284
2918
  await appendToLogAtContext(result.context, {
1285
- event: result.value.replacementLog ? 'session_start' : 'session_start_reconciled',
1286
- session_id: result.value.state.session_id,
2919
+ event: 'session_start_reconciled',
2920
+ session_id: result.value.session_id,
1287
2921
  native_session_id: normalizedNativeSessionId ?? nativeSessionId,
1288
- pid: result.value.state.pid,
1289
- timestamp: result.value.state.native_session_switched_at ?? new Date().toISOString(),
2922
+ pid: result.value.pid,
2923
+ timestamp: new Date().toISOString(),
1290
2924
  }).catch(() => { });
1291
- return result.value.state;
2925
+ return result.value;
1292
2926
  }
1293
2927
  function historyDirectory(context) {
1294
2928
  return join(dirname(context.baseStateDir), 'logs');
@@ -1334,27 +2968,45 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1334
2968
  cause,
1335
2969
  });
1336
2970
  }
2971
+ if (!options.binding) {
2972
+ throw resolvedAbort(context, {
2973
+ code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId,
2974
+ lockPath: context.lockPath, reason: 'A live launch binding is required to end a session pointer.',
2975
+ });
2976
+ }
2977
+ let preLockPointer;
2978
+ traceSessionFinalizationOperation('preLock-read-start');
1337
2979
  try {
1338
- await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
2980
+ preLockPointer = await readSessionPointer(context);
2981
+ traceSessionFinalizationOperation(`preLock-read-done:${preLockPointer.status}`);
1339
2982
  }
1340
2983
  catch (error) {
1341
2984
  throw resolvedAbort(context, {
1342
- code: 'session_pointer_io_failure',
1343
- operation: 'state-dir-create',
1344
- candidateSessionId,
1345
- lockPath: context.lockPath,
1346
- reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
1347
- cause: error,
2985
+ code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
2986
+ lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
1348
2987
  });
1349
2988
  }
2989
+ const preLockAuthorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, preLockPointer, 'preLock');
2990
+ if (!preLockAuthorized)
2991
+ throw unusablePointerAbort(context, candidateSessionId, preLockPointer);
2992
+ traceSessionFinalizationOperation('pre-transaction-auth-start');
2993
+ await authorizeBoundDirectoryBeforeTransaction(options.binding, preLockPointer, options.postLaunchCwd ?? options.binding.context.cwd);
2994
+ traceSessionFinalizationOperation('pre-transaction-auth-done');
1350
2995
  const platform = options.platform ?? process.platform;
1351
2996
  const tracker = { degraded: false };
2997
+ traceSessionFinalizationOperation('acquirePointerLock-start');
1352
2998
  const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
2999
+ traceSessionFinalizationOperation('acquirePointerLock-done');
1353
3000
  let primary;
3001
+ let revalidation = {
3002
+ comparison: { status: 'not-run' }, capability: [],
3003
+ };
1354
3004
  try {
1355
3005
  let pointer;
3006
+ traceSessionFinalizationOperation('under-lock-read-start');
1356
3007
  try {
1357
3008
  pointer = await readSessionPointer(context);
3009
+ traceSessionFinalizationOperation(`under-lock-read-done:${pointer.status}`);
1358
3010
  }
1359
3011
  catch (error) {
1360
3012
  throw resolvedAbort(context, {
@@ -1366,14 +3018,17 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1366
3018
  cause: error,
1367
3019
  });
1368
3020
  }
1369
- if (pointer.status !== 'absent' && pointer.status !== 'usable') {
3021
+ const authorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, pointer, 'under-lock');
3022
+ if (!authorized)
1370
3023
  throw unusablePointerAbort(context, candidateSessionId, pointer);
1371
- }
3024
+ traceSessionFinalizationOperation('under-lock-auth-start');
3025
+ const authorization = await authorizeBoundDirectoryBeforeTransaction(options.binding, pointer, options.postLaunchCwd ?? options.binding.context.cwd);
3026
+ traceSessionFinalizationOperation('under-lock-auth-done');
3027
+ traceSessionFinalizationOperation('under-lock-consume-start');
3028
+ revalidation = await consumeBoundDirectoryAuthorizationUnderLock(options.binding, authorization, pointer);
3029
+ traceSessionFinalizationOperation('under-lock-consume-done');
1372
3030
  const state = pointer.state;
1373
- const ownsCurrentSessionFile = state == null
1374
- || state.session_id === candidateSessionId
1375
- || state.native_session_id === candidateSessionId
1376
- || state.owner_omx_session_id === candidateSessionId;
3031
+ const ownsCurrentSessionFile = true;
1377
3032
  const endTime = new Date().toISOString();
1378
3033
  const historyEntry = {
1379
3034
  session_id: ownsCurrentSessionFile
@@ -1389,12 +3044,19 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1389
3044
  : {}),
1390
3045
  ...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
1391
3046
  };
3047
+ traceSessionFinalizationOperation('history-mkdir-start');
1392
3048
  await nodeMkdir(historyDirectory(context), { recursive: true });
3049
+ traceSessionFinalizationOperation('history-mkdir-done');
3050
+ traceSessionFinalizationOperation('history-append-start');
1393
3051
  await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
3052
+ traceSessionFinalizationOperation('history-append-done');
3053
+ traceSessionFinalizationOperation('hud-cleanup-start');
1394
3054
  await removeDeadSessionHudState(context, [
1395
3055
  ...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
1396
3056
  candidateSessionId,
1397
3057
  ]);
3058
+ traceSessionFinalizationOperation('hud-cleanup-done');
3059
+ traceSessionFinalizationOperation('session-unlink-start');
1398
3060
  if (ownsCurrentSessionFile) {
1399
3061
  try {
1400
3062
  await transactionDependencies.fs.unlink(context.sessionPath);
@@ -1404,6 +3066,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1404
3066
  throw error;
1405
3067
  }
1406
3068
  }
3069
+ traceSessionFinalizationOperation('session-unlink-done');
3070
+ traceSessionFinalizationOperation('session-end-log-start');
1407
3071
  await appendToLogAtContext(context, {
1408
3072
  event: 'session_end',
1409
3073
  session_id: historyEntry.session_id,
@@ -1412,11 +3076,14 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1412
3076
  ...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
1413
3077
  timestamp: endTime,
1414
3078
  }).catch(() => { });
3079
+ traceSessionFinalizationOperation('session-end-log-done');
1415
3080
  }
1416
3081
  catch (error) {
1417
3082
  primary = error;
1418
3083
  }
3084
+ traceSessionFinalizationOperation('lock-release-start');
1419
3085
  const releaseFailures = await releasePointerLock(lock);
3086
+ traceSessionFinalizationOperation('lock-release-done');
1420
3087
  if (primary) {
1421
3088
  if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
1422
3089
  throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
@@ -1426,6 +3093,7 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1426
3093
  if (releaseFailures.length > 0)
1427
3094
  throw releaseFailureError(releaseFailures);
1428
3095
  emitDegradedDurabilityWarning('session pointer end', tracker);
3096
+ return revalidation;
1429
3097
  }
1430
3098
  /** Reset session-scoped HUD/metrics files at launch. */
1431
3099
  export async function resetSessionMetrics(cwd, sessionId) {