oh-my-codex 0.20.0 → 0.20.2

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 (661) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +10 -1
  4. package/dist/adapt/__tests__/hermes.test.js.map +1 -1
  5. package/dist/adapt/contracts.d.ts.map +1 -1
  6. package/dist/agents/native-config.js.map +1 -1
  7. package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
  8. package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
  9. package/dist/auth/__tests__/hotswap.test.js +183 -0
  10. package/dist/auth/__tests__/hotswap.test.js.map +1 -0
  11. package/dist/auth/hotswap.d.ts.map +1 -1
  12. package/dist/auth/hotswap.js +17 -4
  13. package/dist/auth/hotswap.js.map +1 -1
  14. package/dist/auth/paths.js.map +1 -1
  15. package/dist/auth/storage.js.map +1 -1
  16. package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
  17. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  18. package/dist/autopilot/completion-gate.js.map +1 -1
  19. package/dist/autopilot/fsm.d.ts +1 -1
  20. package/dist/autopilot/fsm.d.ts.map +1 -1
  21. package/dist/autoresearch/goal.js.map +1 -1
  22. package/dist/autoresearch/runtime.js.map +1 -1
  23. package/dist/capabilities/lockfile.js.map +1 -1
  24. package/dist/catalog/reader.js.map +1 -1
  25. package/dist/catalog/skill-mirror.js.map +1 -1
  26. package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
  27. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  28. package/dist/cli/__tests__/agents.test.js.map +1 -1
  29. package/dist/cli/__tests__/api.test.js.map +1 -1
  30. package/dist/cli/__tests__/ask.test.js.map +1 -1
  31. package/dist/cli/__tests__/auth.test.js.map +1 -1
  32. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  33. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  34. package/dist/cli/__tests__/capabilities.test.js.map +1 -1
  35. package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
  36. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  37. package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
  38. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-invalid-config.test.js +56 -4
  40. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  41. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  42. package/dist/cli/__tests__/doctor-warning-copy.test.js +735 -14
  43. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  44. package/dist/cli/__tests__/exec.test.js.map +1 -1
  45. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  46. package/dist/cli/__tests__/index.test.js +216 -11
  47. package/dist/cli/__tests__/index.test.js.map +1 -1
  48. package/dist/cli/__tests__/launch-fallback.test.js +49 -3
  49. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  50. package/dist/cli/__tests__/mcp-parity.test.js +84 -12
  51. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  52. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  53. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  54. package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
  55. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  56. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  57. package/dist/cli/__tests__/question.test.js.map +1 -1
  58. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  59. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  60. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  61. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  62. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  63. package/dist/cli/__tests__/ralph.test.js +8 -3
  64. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  65. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  66. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  67. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
  68. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  69. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  70. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  71. package/dist/cli/__tests__/ralplan.test.js +129 -0
  72. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  73. package/dist/cli/__tests__/resume.test.js +6 -1
  74. package/dist/cli/__tests__/resume.test.js.map +1 -1
  75. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  76. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  77. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  78. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  79. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  80. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  81. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  82. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +131 -5
  83. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  84. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  85. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  86. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  87. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  88. package/dist/cli/__tests__/setup-install-mode.test.js +2264 -17
  89. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  90. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  91. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  92. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  93. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  94. package/dist/cli/__tests__/setup-refresh.test.js +168 -5
  95. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  96. package/dist/cli/__tests__/setup-scope.test.js +73 -6
  97. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  98. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  99. package/dist/cli/__tests__/state.test.js +37 -0
  100. package/dist/cli/__tests__/state.test.js.map +1 -1
  101. package/dist/cli/__tests__/team.test.js.map +1 -1
  102. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  103. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  104. package/dist/cli/__tests__/uninstall.test.js +2067 -26
  105. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  106. package/dist/cli/__tests__/update.test.js +58 -0
  107. package/dist/cli/__tests__/update.test.js.map +1 -1
  108. package/dist/cli/__tests__/url.test.js.map +1 -1
  109. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  110. package/dist/cli/adapt.js.map +1 -1
  111. package/dist/cli/agents-init.js.map +1 -1
  112. package/dist/cli/agents.js.map +1 -1
  113. package/dist/cli/api.js.map +1 -1
  114. package/dist/cli/ask.d.ts +1 -1
  115. package/dist/cli/ask.d.ts.map +1 -1
  116. package/dist/cli/ask.js.map +1 -1
  117. package/dist/cli/auth.js.map +1 -1
  118. package/dist/cli/autoresearch-guided.js.map +1 -1
  119. package/dist/cli/autoresearch-intake.js.map +1 -1
  120. package/dist/cli/capabilities.js.map +1 -1
  121. package/dist/cli/cleanup.js.map +1 -1
  122. package/dist/cli/codex-feature-probe.js.map +1 -1
  123. package/dist/cli/codex-home.js.map +1 -1
  124. package/dist/cli/doctor.d.ts +20 -0
  125. package/dist/cli/doctor.d.ts.map +1 -1
  126. package/dist/cli/doctor.js +636 -161
  127. package/dist/cli/doctor.js.map +1 -1
  128. package/dist/cli/explore.js.map +1 -1
  129. package/dist/cli/index.d.ts +11 -3
  130. package/dist/cli/index.d.ts.map +1 -1
  131. package/dist/cli/index.js +108 -20
  132. package/dist/cli/index.js.map +1 -1
  133. package/dist/cli/mcp-serve.js.map +1 -1
  134. package/dist/cli/mission.js.map +1 -1
  135. package/dist/cli/native-assets.js.map +1 -1
  136. package/dist/cli/native-hook-claim-journal.d.ts +18 -0
  137. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  138. package/dist/cli/native-hook-claim-journal.js +239 -0
  139. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  140. package/dist/cli/omx.js.map +1 -1
  141. package/dist/cli/plugin-marketplace.js.map +1 -1
  142. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  143. package/dist/cli/question.js.map +1 -1
  144. package/dist/cli/ralph.d.ts.map +1 -1
  145. package/dist/cli/ralph.js +3 -2
  146. package/dist/cli/ralph.js.map +1 -1
  147. package/dist/cli/ralplan.d.ts +14 -0
  148. package/dist/cli/ralplan.d.ts.map +1 -0
  149. package/dist/cli/ralplan.js +203 -0
  150. package/dist/cli/ralplan.js.map +1 -0
  151. package/dist/cli/setup-preferences.d.ts +11 -0
  152. package/dist/cli/setup-preferences.d.ts.map +1 -1
  153. package/dist/cli/setup-preferences.js +32 -0
  154. package/dist/cli/setup-preferences.js.map +1 -1
  155. package/dist/cli/setup.d.ts +53 -0
  156. package/dist/cli/setup.d.ts.map +1 -1
  157. package/dist/cli/setup.js +1403 -400
  158. package/dist/cli/setup.js.map +1 -1
  159. package/dist/cli/sparkshell.js.map +1 -1
  160. package/dist/cli/star-prompt.js.map +1 -1
  161. package/dist/cli/state.d.ts.map +1 -1
  162. package/dist/cli/state.js +2 -1
  163. package/dist/cli/state.js.map +1 -1
  164. package/dist/cli/team.js.map +1 -1
  165. package/dist/cli/ultragoal.d.ts +5 -1
  166. package/dist/cli/ultragoal.d.ts.map +1 -1
  167. package/dist/cli/ultragoal.js +34 -2
  168. package/dist/cli/ultragoal.js.map +1 -1
  169. package/dist/cli/uninstall.d.ts +8 -0
  170. package/dist/cli/uninstall.d.ts.map +1 -1
  171. package/dist/cli/uninstall.js +947 -99
  172. package/dist/cli/uninstall.js.map +1 -1
  173. package/dist/cli/update.d.ts.map +1 -1
  174. package/dist/cli/update.js +8 -1
  175. package/dist/cli/update.js.map +1 -1
  176. package/dist/cli/url.js.map +1 -1
  177. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  178. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  179. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  180. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  181. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  182. package/dist/config/__tests__/generator-idempotent.test.js +1049 -135
  183. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  184. package/dist/config/__tests__/generator-notify.test.js +28 -49
  185. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  186. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  187. package/dist/config/codex-feature-flags.js.map +1 -1
  188. package/dist/config/codex-hooks.d.ts +124 -1
  189. package/dist/config/codex-hooks.d.ts.map +1 -1
  190. package/dist/config/codex-hooks.js +2142 -265
  191. package/dist/config/codex-hooks.js.map +1 -1
  192. package/dist/config/commit-lore-guard.js.map +1 -1
  193. package/dist/config/generator.d.ts +40 -26
  194. package/dist/config/generator.d.ts.map +1 -1
  195. package/dist/config/generator.js +1159 -288
  196. package/dist/config/generator.js.map +1 -1
  197. package/dist/config/mcp-registry.js.map +1 -1
  198. package/dist/config/models.d.ts +2 -2
  199. package/dist/config/models.d.ts.map +1 -1
  200. package/dist/config/models.js.map +1 -1
  201. package/dist/config/omx-first-party-mcp.js.map +1 -1
  202. package/dist/config/team-mode.d.ts.map +1 -1
  203. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  204. package/dist/document-refresh/enforcer.js.map +1 -1
  205. package/dist/exec/followup.js.map +1 -1
  206. package/dist/goal-workflows/artifacts.js.map +1 -1
  207. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  208. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  209. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  210. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  211. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  212. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  213. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  214. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  215. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  216. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  217. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  218. package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
  219. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  220. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
  221. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  222. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  223. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
  224. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  225. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  226. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  227. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  228. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  229. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  230. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  231. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  232. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  233. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  234. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  235. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  236. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  237. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  238. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  239. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  240. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
  241. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  242. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  243. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  244. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  245. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  246. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  247. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  248. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  249. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  250. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  251. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  252. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  253. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  254. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  255. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  256. package/dist/hooks/__tests__/session.test.js +532 -11
  257. package/dist/hooks/__tests__/session.test.js.map +1 -1
  258. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  259. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  260. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  261. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  262. package/dist/hooks/agents-overlay.js +5 -5
  263. package/dist/hooks/agents-overlay.js.map +1 -1
  264. package/dist/hooks/code-simplifier/index.js.map +1 -1
  265. package/dist/hooks/explore-routing.js.map +1 -1
  266. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  267. package/dist/hooks/extensibility/events.js.map +1 -1
  268. package/dist/hooks/extensibility/loader.js.map +1 -1
  269. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  270. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  271. package/dist/hooks/keyword-detector.d.ts +33 -7
  272. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  273. package/dist/hooks/keyword-detector.js +2228 -119
  274. package/dist/hooks/keyword-detector.js.map +1 -1
  275. package/dist/hooks/keyword-registry.d.ts +12 -0
  276. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  277. package/dist/hooks/keyword-registry.js +41 -0
  278. package/dist/hooks/keyword-registry.js.map +1 -1
  279. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  280. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  281. package/dist/hooks/prompt-session-provenance.js +180 -0
  282. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  283. package/dist/hooks/session.d.ts +139 -34
  284. package/dist/hooks/session.d.ts.map +1 -1
  285. package/dist/hooks/session.js +1239 -317
  286. package/dist/hooks/session.js.map +1 -1
  287. package/dist/hooks/task-size-detector.js.map +1 -1
  288. package/dist/hud/__tests__/state.test.js.map +1 -1
  289. package/dist/hud/authority.js.map +1 -1
  290. package/dist/hud/constants.js.map +1 -1
  291. package/dist/hud/index.js.map +1 -1
  292. package/dist/hud/reconcile.js.map +1 -1
  293. package/dist/hud/render.js.map +1 -1
  294. package/dist/hud/state.js.map +1 -1
  295. package/dist/hud/tmux.js.map +1 -1
  296. package/dist/leader/__tests__/contract.test.js +146 -2
  297. package/dist/leader/__tests__/contract.test.js.map +1 -1
  298. package/dist/leader/contract.d.ts +26 -5
  299. package/dist/leader/contract.d.ts.map +1 -1
  300. package/dist/leader/contract.js +131 -7
  301. package/dist/leader/contract.js.map +1 -1
  302. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  303. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  304. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  305. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  306. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  307. package/dist/mcp/bootstrap.js.map +1 -1
  308. package/dist/mcp/code-intel-server.d.ts +58 -58
  309. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  310. package/dist/mcp/code-intel-server.js.map +1 -1
  311. package/dist/mcp/hermes-bridge.js.map +1 -1
  312. package/dist/mcp/hermes-server.d.ts +70 -70
  313. package/dist/mcp/hermes-server.d.ts.map +1 -1
  314. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  315. package/dist/mcp/memory-server.d.ts +34 -34
  316. package/dist/mcp/memory-server.d.ts.map +1 -1
  317. package/dist/mcp/memory-server.js.map +1 -1
  318. package/dist/mcp/state-paths.d.ts +14 -0
  319. package/dist/mcp/state-paths.d.ts.map +1 -1
  320. package/dist/mcp/state-paths.js +96 -20
  321. package/dist/mcp/state-paths.js.map +1 -1
  322. package/dist/mcp/state-server.d.ts +47 -47
  323. package/dist/mcp/state-server.d.ts.map +1 -1
  324. package/dist/mcp/trace-server.d.ts +4 -4
  325. package/dist/mcp/trace-server.d.ts.map +1 -1
  326. package/dist/mcp/trace-server.js.map +1 -1
  327. package/dist/mcp/wiki-server.d.ts +23 -23
  328. package/dist/mcp/wiki-server.d.ts.map +1 -1
  329. package/dist/mcp/wiki-server.js.map +1 -1
  330. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  331. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  332. package/dist/modes/base.d.ts.map +1 -1
  333. package/dist/modes/base.js +5 -5
  334. package/dist/modes/base.js.map +1 -1
  335. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  336. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  337. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  338. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  339. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  340. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  341. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  342. package/dist/notifications/http-client.js.map +1 -1
  343. package/dist/notifications/index.d.ts +4 -1
  344. package/dist/notifications/index.d.ts.map +1 -1
  345. package/dist/notifications/index.js +3 -9
  346. package/dist/notifications/index.js.map +1 -1
  347. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  348. package/dist/notifications/reply-listener.d.ts.map +1 -1
  349. package/dist/notifications/reply-listener.js.map +1 -1
  350. package/dist/notifications/session-registry.js.map +1 -1
  351. package/dist/notifications/session-status.js.map +1 -1
  352. package/dist/notifications/temp-contract.js.map +1 -1
  353. package/dist/notifications/tmux-detector.js.map +1 -1
  354. package/dist/notifications/tmux.js.map +1 -1
  355. package/dist/openclaw/config.js.map +1 -1
  356. package/dist/openclaw/dispatcher.js.map +1 -1
  357. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  358. package/dist/pipeline/__tests__/stages.test.js +8 -5
  359. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  360. package/dist/pipeline/stages/code-review.js.map +1 -1
  361. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  362. package/dist/pipeline/stages/ralplan.js.map +1 -1
  363. package/dist/pipeline/stages/team-exec.js.map +1 -1
  364. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  365. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  366. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  367. package/dist/planning/artifact-names.js.map +1 -1
  368. package/dist/planning/artifacts.js.map +1 -1
  369. package/dist/question/__tests__/state.test.js.map +1 -1
  370. package/dist/question/__tests__/ui.test.js.map +1 -1
  371. package/dist/question/autopilot-wait.js.map +1 -1
  372. package/dist/question/client.d.ts.map +1 -1
  373. package/dist/question/client.js.map +1 -1
  374. package/dist/question/deep-interview.js.map +1 -1
  375. package/dist/question/events.js.map +1 -1
  376. package/dist/question/renderer.js.map +1 -1
  377. package/dist/question/state.d.ts.map +1 -1
  378. package/dist/question/state.js.map +1 -1
  379. package/dist/question/ui.js.map +1 -1
  380. package/dist/ralph/contract.d.ts +1 -1
  381. package/dist/ralph/contract.d.ts.map +1 -1
  382. package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
  383. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  384. package/dist/ralplan/__tests__/runtime.test.js +112 -14
  385. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  386. package/dist/ralplan/consensus-gate.d.ts +3 -3
  387. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  388. package/dist/ralplan/consensus-gate.js +155 -60
  389. package/dist/ralplan/consensus-gate.js.map +1 -1
  390. package/dist/ralplan/runtime.d.ts +2 -2
  391. package/dist/ralplan/runtime.d.ts.map +1 -1
  392. package/dist/ralplan/runtime.js +1 -2
  393. package/dist/ralplan/runtime.js.map +1 -1
  394. package/dist/runtime/bridge.d.ts.map +1 -1
  395. package/dist/runtime/bridge.js.map +1 -1
  396. package/dist/runtime/process-tree.js.map +1 -1
  397. package/dist/runtime/run-loop.js.map +1 -1
  398. package/dist/runtime/run-outcome.d.ts +3 -3
  399. package/dist/runtime/run-outcome.d.ts.map +1 -1
  400. package/dist/runtime/run-outcome.js.map +1 -1
  401. package/dist/runtime/run-state.js.map +1 -1
  402. package/dist/scripts/__tests__/codex-native-hook.test.js +5414 -801
  403. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  404. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  405. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  406. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  407. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  408. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  409. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  410. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  411. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  412. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  413. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  414. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  415. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  416. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  417. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  418. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  419. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
  420. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  421. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  422. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  423. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
  424. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  425. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  426. package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
  427. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  428. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  429. package/dist/scripts/build-api.js.map +1 -1
  430. package/dist/scripts/build-explore-harness.js.map +1 -1
  431. package/dist/scripts/build-sparkshell.js.map +1 -1
  432. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  433. package/dist/scripts/codex-execution-surface.js.map +1 -1
  434. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  435. package/dist/scripts/codex-native-hook.js +825 -172
  436. package/dist/scripts/codex-native-hook.js.map +1 -1
  437. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  438. package/dist/scripts/generate-release-body.js.map +1 -1
  439. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  440. package/dist/scripts/hook-payload-guard.js.map +1 -1
  441. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  442. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  443. package/dist/scripts/notify-fallback-delivery.js +419 -0
  444. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  445. package/dist/scripts/notify-fallback-watcher.js +136 -30
  446. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  447. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  448. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  449. package/dist/scripts/notify-hook/auto-nudge.js +39 -13
  450. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  451. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  452. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  453. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  454. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  455. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  456. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  457. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  458. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  459. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  460. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  461. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  462. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  463. package/dist/scripts/notify-hook/state-io.js +73 -11
  464. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
  466. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  467. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  468. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
  469. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  470. package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
  471. package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
  472. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  473. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  474. package/dist/scripts/notify-hook/tmux-injection.js +25 -16
  475. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  476. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  477. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  478. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  479. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  480. package/dist/scripts/notify-hook.d.ts +24 -1
  481. package/dist/scripts/notify-hook.d.ts.map +1 -1
  482. package/dist/scripts/notify-hook.js +266 -90
  483. package/dist/scripts/notify-hook.js.map +1 -1
  484. package/dist/scripts/postinstall.js.map +1 -1
  485. package/dist/scripts/prompt-inventory.js.map +1 -1
  486. package/dist/scripts/smoke-packed-install.d.ts +879 -2
  487. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  488. package/dist/scripts/smoke-packed-install.js +1528 -7
  489. package/dist/scripts/smoke-packed-install.js.map +1 -1
  490. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  491. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  492. package/dist/scripts/test-sparkshell.js.map +1 -1
  493. package/dist/scripts/verify-native-agents.js.map +1 -1
  494. package/dist/session-history/friction.js.map +1 -1
  495. package/dist/session-history/search.js.map +1 -1
  496. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  497. package/dist/sidecar/collector.js.map +1 -1
  498. package/dist/sidecar/index.js.map +1 -1
  499. package/dist/sidecar/render.js.map +1 -1
  500. package/dist/sidecar/tmux.js.map +1 -1
  501. package/dist/state/__tests__/operations.test.js +64 -15
  502. package/dist/state/__tests__/operations.test.js.map +1 -1
  503. package/dist/state/__tests__/skill-active.test.js +19 -1
  504. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  505. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  506. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  507. package/dist/state/mode-state-context.js.map +1 -1
  508. package/dist/state/operations.d.ts +1 -1
  509. package/dist/state/operations.d.ts.map +1 -1
  510. package/dist/state/operations.js +49 -55
  511. package/dist/state/operations.js.map +1 -1
  512. package/dist/state/paths.d.ts +1 -1
  513. package/dist/state/paths.d.ts.map +1 -1
  514. package/dist/state/paths.js +1 -1
  515. package/dist/state/paths.js.map +1 -1
  516. package/dist/state/skill-active.d.ts +13 -1
  517. package/dist/state/skill-active.d.ts.map +1 -1
  518. package/dist/state/skill-active.js +43 -4
  519. package/dist/state/skill-active.js.map +1 -1
  520. package/dist/state/terminal-normalization.js.map +1 -1
  521. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  522. package/dist/state/workflow-transition-reconcile.js +20 -4
  523. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  524. package/dist/state/workflow-transition.d.ts +1 -1
  525. package/dist/state/workflow-transition.d.ts.map +1 -1
  526. package/dist/state/workflow-transition.js.map +1 -1
  527. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
  528. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
  529. package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
  530. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
  531. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  532. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  533. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
  534. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  535. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  536. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  537. package/dist/subagents/adapted-role-binding.d.ts +17 -0
  538. package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
  539. package/dist/subagents/adapted-role-binding.js +75 -0
  540. package/dist/subagents/adapted-role-binding.js.map +1 -0
  541. package/dist/subagents/role-routing-marker.d.ts +10 -0
  542. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  543. package/dist/subagents/role-routing-marker.js +136 -0
  544. package/dist/subagents/role-routing-marker.js.map +1 -0
  545. package/dist/subagents/tracker.d.ts +172 -0
  546. package/dist/subagents/tracker.d.ts.map +1 -1
  547. package/dist/subagents/tracker.js +1077 -61
  548. package/dist/subagents/tracker.js.map +1 -1
  549. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  550. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  551. package/dist/team/__tests__/model-contract.test.js +231 -1
  552. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  553. package/dist/team/__tests__/role-router.test.js.map +1 -1
  554. package/dist/team/__tests__/runtime.test.js +401 -1
  555. package/dist/team/__tests__/runtime.test.js.map +1 -1
  556. package/dist/team/__tests__/scaling.test.js +181 -0
  557. package/dist/team/__tests__/scaling.test.js.map +1 -1
  558. package/dist/team/__tests__/state-root.test.js.map +1 -1
  559. package/dist/team/__tests__/tmux-session.test.js +434 -4
  560. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  561. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  562. package/dist/team/api-interop.d.ts +2 -2
  563. package/dist/team/api-interop.d.ts.map +1 -1
  564. package/dist/team/api-interop.js.map +1 -1
  565. package/dist/team/commit-hygiene.js.map +1 -1
  566. package/dist/team/contracts.d.ts +5 -5
  567. package/dist/team/contracts.d.ts.map +1 -1
  568. package/dist/team/delivery-log.js.map +1 -1
  569. package/dist/team/followup-planner.js.map +1 -1
  570. package/dist/team/goal-workflow.js.map +1 -1
  571. package/dist/team/idle-nudge.d.ts.map +1 -1
  572. package/dist/team/model-contract.d.ts +20 -1
  573. package/dist/team/model-contract.d.ts.map +1 -1
  574. package/dist/team/model-contract.js +267 -30
  575. package/dist/team/model-contract.js.map +1 -1
  576. package/dist/team/orchestrator.js.map +1 -1
  577. package/dist/team/pane-status.js.map +1 -1
  578. package/dist/team/phase-controller.js.map +1 -1
  579. package/dist/team/reminder-intents.d.ts +1 -1
  580. package/dist/team/reminder-intents.d.ts.map +1 -1
  581. package/dist/team/reminder-intents.js.map +1 -1
  582. package/dist/team/runtime-cli.js.map +1 -1
  583. package/dist/team/runtime.d.ts.map +1 -1
  584. package/dist/team/runtime.js +83 -52
  585. package/dist/team/runtime.js.map +1 -1
  586. package/dist/team/scaling.d.ts +9 -7
  587. package/dist/team/scaling.d.ts.map +1 -1
  588. package/dist/team/scaling.js +69 -28
  589. package/dist/team/scaling.js.map +1 -1
  590. package/dist/team/state/dispatch-lock.js.map +1 -1
  591. package/dist/team/state/dispatch.js.map +1 -1
  592. package/dist/team/state/events.js.map +1 -1
  593. package/dist/team/state/tasks.js.map +1 -1
  594. package/dist/team/state-root.js.map +1 -1
  595. package/dist/team/state.js.map +1 -1
  596. package/dist/team/team-identity.d.ts.map +1 -1
  597. package/dist/team/team-identity.js.map +1 -1
  598. package/dist/team/tmux-session.d.ts +2 -0
  599. package/dist/team/tmux-session.d.ts.map +1 -1
  600. package/dist/team/tmux-session.js +90 -42
  601. package/dist/team/tmux-session.js.map +1 -1
  602. package/dist/team/worker-bootstrap.js.map +1 -1
  603. package/dist/team/worktree.js.map +1 -1
  604. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  605. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  606. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  607. package/dist/ultragoal/artifacts.js +4 -1
  608. package/dist/ultragoal/artifacts.js.map +1 -1
  609. package/dist/url-reader/index.js.map +1 -1
  610. package/dist/utils/__tests__/agents-md.test.js +16 -0
  611. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  612. package/dist/utils/agents-md.d.ts.map +1 -1
  613. package/dist/utils/agents-md.js +4 -3
  614. package/dist/utils/agents-md.js.map +1 -1
  615. package/dist/utils/agents-model-table.js.map +1 -1
  616. package/dist/utils/package.js.map +1 -1
  617. package/dist/utils/paths.js.map +1 -1
  618. package/dist/utils/platform-command.js.map +1 -1
  619. package/dist/utils/repo-deps.js.map +1 -1
  620. package/dist/utils/version.js.map +1 -1
  621. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  622. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  623. package/dist/verification/verifier.js.map +1 -1
  624. package/dist/visual/constants.d.ts +1 -1
  625. package/dist/visual/constants.d.ts.map +1 -1
  626. package/dist/vscode/__tests__/index.test.js.map +1 -1
  627. package/dist/vscode/index.js.map +1 -1
  628. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  629. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  630. package/dist/wiki/__tests__/query.test.js.map +1 -1
  631. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  632. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  633. package/dist/wiki/lint.js.map +1 -1
  634. package/dist/wiki/query.js.map +1 -1
  635. package/dist/wiki/storage.js.map +1 -1
  636. package/package.json +2 -2
  637. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  638. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  639. package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
  640. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
  641. package/skills/omx-setup/SKILL.md +10 -5
  642. package/skills/ralph/SKILL.md +4 -2
  643. package/skills/ralplan/SKILL.md +5 -2
  644. package/src/scripts/__tests__/codex-native-hook.test.ts +20497 -12377
  645. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  646. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  647. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  648. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
  649. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
  650. package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
  651. package/src/scripts/codex-native-hook.ts +918 -172
  652. package/src/scripts/notify-fallback-delivery.ts +416 -0
  653. package/src/scripts/notify-fallback-watcher.ts +132 -40
  654. package/src/scripts/notify-hook/auto-nudge.ts +43 -12
  655. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  656. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  657. package/src/scripts/notify-hook/state-io.ts +100 -13
  658. package/src/scripts/notify-hook/tmux-injection.ts +38 -21
  659. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  660. package/src/scripts/notify-hook.ts +335 -90
  661. package/src/scripts/smoke-packed-install.ts +1829 -6
@@ -1,6 +1,190 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
6
+ import { hostname, tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
2
8
  import { describe, it } from 'node:test';
3
- import { buildSubagentResumeLedger, createSubagentTrackingState, recordSubagentTurn, selectReusableSubagentEntry, summarizeSubagentSession, } from '../tracker.js';
9
+ import { getBaseStateDir } from '../../state/paths.js';
10
+ import { canonicalizeOriginCwd } from '../../leader/contract.js';
11
+ import { __setCrossProcessPublishBarrierForTest, __setCrossProcessQuarantineBarrierForTest, CrossProcessLockLostError, bindPendingRoleIntentUnderLock, buildSubagentResumeLedger, completeAdaptedRoleBinding, CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP, CROSS_PROCESS_LOCK_LEASE_MS, createSubagentTrackingState, crossProcessLockPath, consumePendingRoleIntent, recordSubagentTurn, NATIVE_SUBAGENT_PROVENANCE, OMX_ADAPTED_PROVENANCE, readProcessStartIdentity, readSubagentTrackingState, recordPendingRoleIntent, selectReusableSubagentEntry, summarizeSubagentSession, withCrossProcessFileLockSync } from '../tracker.js';
12
+ import { subagentTrackingPath } from '../tracker.js';
13
+ import { NATIVE_SUBAGENT_ROLE_ROUTING_MARKER_FILE, readRoleRoutingMarker, writeRoleRoutingMarker } from '../role-routing-marker.js';
14
+ const credentialDigest = (value) => createHash('sha256').update(value).digest('hex');
15
+ const canonicalCorrelationToken = (value) => credentialDigest(value).slice(0, 32);
16
+ const canonicalClaimantToken = (value) => {
17
+ const digest = credentialDigest(value);
18
+ return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-4${digest.slice(13, 16)}-8${digest.slice(17, 20)}-${digest.slice(20, 32)}`;
19
+ };
20
+ const PENDING_ROLE_INTENT_WORKER_SOURCE = `
21
+ import { existsSync } from 'node:fs';
22
+
23
+ const input = JSON.parse(process.env.OMX_PENDING_ROLE_INTENT_WORKER_INPUT ?? '{}');
24
+ const tracker = await import(process.env.OMX_TRACKER_MODULE_URL ?? '');
25
+ const startSignal = process.env.OMX_PENDING_ROLE_INTENT_START_SIGNAL ?? '';
26
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
27
+ const startedAt = Date.now();
28
+
29
+ process.stdout.write('ready\\n');
30
+ while (!existsSync(startSignal)) {
31
+ if (Date.now() - startedAt > 10_000) {
32
+ throw new Error('Timed out waiting for pending role intent test start signal');
33
+ }
34
+ Atomics.wait(waitArray, 0, 0, 5);
35
+ }
36
+
37
+ const result = input.operation === 'record'
38
+ ? tracker.recordPendingRoleIntent(input.cwd, input)
39
+ : input.operation === 'consume'
40
+ ? tracker.consumePendingRoleIntent(input.cwd, input)
41
+ : (await tracker.recordSubagentTurnForSession(input.cwd, input), { ok: true });
42
+ process.stdout.write(JSON.stringify(result));
43
+ `;
44
+ function spawnPendingRoleIntentWorker(startSignal, input) {
45
+ const child = spawn(process.execPath, ['--input-type=module', '--eval', PENDING_ROLE_INTENT_WORKER_SOURCE], {
46
+ env: {
47
+ ...process.env,
48
+ OMX_PENDING_ROLE_INTENT_START_SIGNAL: startSignal,
49
+ OMX_PENDING_ROLE_INTENT_WORKER_INPUT: JSON.stringify(input),
50
+ OMX_TRACKER_MODULE_URL: new URL('../tracker.js', import.meta.url).href,
51
+ },
52
+ stdio: ['ignore', 'pipe', 'pipe'],
53
+ });
54
+ if (!child.stdout || !child.stderr) {
55
+ throw new Error('Pending role intent test worker did not expose output streams');
56
+ }
57
+ let stdout = '';
58
+ let stderr = '';
59
+ let ready = false;
60
+ let resolveReady;
61
+ let rejectReady;
62
+ let resolveResult;
63
+ let rejectResult;
64
+ const readyPromise = new Promise((resolve, reject) => {
65
+ resolveReady = resolve;
66
+ rejectReady = reject;
67
+ });
68
+ const resultPromise = new Promise((resolve, reject) => {
69
+ resolveResult = resolve;
70
+ rejectResult = reject;
71
+ });
72
+ child.stdout.setEncoding('utf-8');
73
+ child.stdout.on('data', (chunk) => {
74
+ stdout += chunk;
75
+ if (!ready && stdout.includes('ready\n')) {
76
+ ready = true;
77
+ resolveReady();
78
+ }
79
+ });
80
+ child.stderr.setEncoding('utf-8');
81
+ child.stderr.on('data', (chunk) => {
82
+ stderr += chunk;
83
+ });
84
+ child.once('error', (error) => {
85
+ if (!ready)
86
+ rejectReady(error);
87
+ rejectResult(error);
88
+ });
89
+ child.once('close', (code) => {
90
+ if (!ready) {
91
+ rejectReady(new Error(`Pending role intent test worker exited before ready: ${stderr || `code ${code}`}`));
92
+ }
93
+ if (code !== 0) {
94
+ rejectResult(new Error(`Pending role intent test worker failed: ${stderr || `code ${code}`}`));
95
+ return;
96
+ }
97
+ try {
98
+ const resultJson = stdout.slice(stdout.indexOf('ready\n') + 'ready\n'.length);
99
+ resolveResult(JSON.parse(resultJson));
100
+ }
101
+ catch (error) {
102
+ rejectResult(error instanceof Error ? error : new Error(String(error)));
103
+ }
104
+ });
105
+ return { ready: readyPromise, result: resultPromise };
106
+ }
107
+ async function runConcurrentPendingRoleIntentWorkers(cwd, inputs) {
108
+ const startSignal = join(cwd, 'pending-role-intent-workers-start');
109
+ const workers = inputs.map((input) => spawnPendingRoleIntentWorker(startSignal, input));
110
+ await Promise.all(workers.map((worker) => worker.ready));
111
+ await writeFile(startSignal, 'start\n');
112
+ return Promise.all(workers.map((worker) => worker.result));
113
+ }
114
+ function isSuccessfulRoleIntentRecord(result) {
115
+ return result !== null && 'ok' in result && result.ok === true;
116
+ }
117
+ function isSingleFlightConflict(result) {
118
+ return result !== null && 'ok' in result && result.ok === false && result.reason === 'single_flight_conflict';
119
+ }
120
+ function isConsumedRoleIntent(result) {
121
+ return result !== null && 'role' in result;
122
+ }
123
+ const CROSS_PROCESS_LOCK_HOLDER_SOURCE = `
124
+ const tracker = await import(process.env.OMX_TRACKER_MODULE_URL ?? '');
125
+ const { existsSync, writeFileSync } = await import('node:fs');
126
+ const resourcePath = process.env.OMX_LOCK_RESOURCE_PATH ?? '';
127
+ const readyPath = process.env.OMX_LOCK_READY_PATH ?? '';
128
+ const releasePath = process.env.OMX_LOCK_RELEASE_PATH ?? '';
129
+ const publicationPath = process.env.OMX_LOCK_PUBLICATION_PATH ?? '';
130
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
131
+
132
+ tracker.withCrossProcessFileLockSync(resourcePath, () => {
133
+ if (publicationPath) writeFileSync(publicationPath, 'successor\\n');
134
+ writeFileSync(readyPath, 'ready\\n');
135
+ const deadline = Date.now() + 5_000;
136
+ while (!existsSync(releasePath)) {
137
+ if (Date.now() >= deadline) throw new Error('Timed out waiting to release successor lock');
138
+ Atomics.wait(waitArray, 0, 0, 5);
139
+ }
140
+ });
141
+ `;
142
+ function crossProcessLockClaim(token, pid, acquiredAtMs, host = hostname(), pidStartId) {
143
+ return `${JSON.stringify({
144
+ token,
145
+ pid,
146
+ host,
147
+ acquired_at: new Date(acquiredAtMs).toISOString(),
148
+ ...(pidStartId ? { pid_start_id: pidStartId } : {}),
149
+ })}\n`;
150
+ }
151
+ function spawnCrossProcessLockHolder(resourcePath, readyPath, releasePath, publicationPath) {
152
+ return spawn(process.execPath, ['--input-type=module', '--eval', CROSS_PROCESS_LOCK_HOLDER_SOURCE], {
153
+ env: {
154
+ ...process.env,
155
+ OMX_TRACKER_MODULE_URL: new URL('../tracker.js', import.meta.url).href,
156
+ OMX_LOCK_RESOURCE_PATH: resourcePath,
157
+ OMX_LOCK_READY_PATH: readyPath,
158
+ OMX_LOCK_RELEASE_PATH: releasePath,
159
+ ...(publicationPath ? { OMX_LOCK_PUBLICATION_PATH: publicationPath } : {}),
160
+ },
161
+ });
162
+ }
163
+ function waitForFileSync(path, timeoutMs = 1_000) {
164
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
165
+ const deadline = Date.now() + timeoutMs;
166
+ while (!existsSync(path)) {
167
+ if (Date.now() >= deadline)
168
+ throw new Error(`Timed out waiting for ${path}`);
169
+ Atomics.wait(waitArray, 0, 0, 5);
170
+ }
171
+ }
172
+ async function waitForChildExit(child) {
173
+ if (child.exitCode !== null)
174
+ return;
175
+ await new Promise((resolve, reject) => {
176
+ child.once('error', reject);
177
+ child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`Lock-owner child exited with code ${code}`)));
178
+ });
179
+ }
180
+ async function stopChild(child) {
181
+ if (!child || child.exitCode !== null)
182
+ return;
183
+ await new Promise((resolve) => {
184
+ child.once('exit', resolve);
185
+ child.kill();
186
+ });
187
+ }
4
188
  describe('subagents/tracker', () => {
5
189
  it('tracks leader and subagent threads per session and computes active windows', () => {
6
190
  let state = createSubagentTrackingState();
@@ -36,8 +220,20 @@ describe('subagents/tracker', () => {
36
220
  allSubagentThreadIds: ['sub-thread-1', 'sub-thread-2'],
37
221
  activeSubagentThreadIds: ['sub-thread-1', 'sub-thread-2'],
38
222
  savedSubagents: [
39
- { agentId: 'sub-thread-1', threadId: 'sub-thread-1', role: 'ralph', laneId: 'ralph', status: 'available' },
40
- { agentId: 'sub-thread-2', threadId: 'sub-thread-2', role: 'ralph', laneId: 'ralph', status: 'available' },
223
+ {
224
+ agentId: 'sub-thread-1',
225
+ threadId: 'sub-thread-1',
226
+ role: 'ralph',
227
+ laneId: 'ralph',
228
+ status: 'available',
229
+ },
230
+ {
231
+ agentId: 'sub-thread-2',
232
+ threadId: 'sub-thread-2',
233
+ role: 'ralph',
234
+ laneId: 'ralph',
235
+ status: 'available',
236
+ },
41
237
  ],
42
238
  updatedAt: '2026-03-17T00:01:00.000Z',
43
239
  });
@@ -254,8 +450,20 @@ describe('subagents/tracker', () => {
254
450
  });
255
451
  assert.deepEqual(summary?.activeSubagentThreadIds, []);
256
452
  assert.deepEqual(summary?.savedSubagents, [
257
- { agentId: 'sub-thread-closed', threadId: 'sub-thread-closed', role: 'critic', laneId: 'critic', status: 'closed' },
258
- { agentId: 'sub-thread-unavailable', threadId: 'sub-thread-unavailable', role: 'architect', laneId: 'architect', status: 'unavailable' },
453
+ {
454
+ agentId: 'sub-thread-closed',
455
+ threadId: 'sub-thread-closed',
456
+ role: 'critic',
457
+ laneId: 'critic',
458
+ status: 'closed',
459
+ },
460
+ {
461
+ agentId: 'sub-thread-unavailable',
462
+ threadId: 'sub-thread-unavailable',
463
+ role: 'architect',
464
+ laneId: 'architect',
465
+ status: 'unavailable',
466
+ },
259
467
  ]);
260
468
  assert.deepEqual(ledger?.activeSubagentThreadIds, []);
261
469
  });
@@ -295,7 +503,13 @@ describe('subagents/tracker', () => {
295
503
  const thread = state.sessions['sess-1']?.threads['sub-thread-1'];
296
504
  assert.deepEqual(summary?.activeSubagentThreadIds, ['sub-thread-1']);
297
505
  assert.deepEqual(summary?.savedSubagents, [
298
- { agentId: 'sub-thread-1', threadId: 'sub-thread-1', role: 'architect', laneId: 'architect', status: 'available' },
506
+ {
507
+ agentId: 'sub-thread-1',
508
+ threadId: 'sub-thread-1',
509
+ role: 'architect',
510
+ laneId: 'architect',
511
+ status: 'available',
512
+ },
299
513
  ]);
300
514
  assert.deepEqual(ledger?.activeSubagentThreadIds, ['sub-thread-1']);
301
515
  assert.equal(ledger?.savedSubagents[0]?.status, 'available');
@@ -320,6 +534,7 @@ describe('subagents/tracker', () => {
320
534
  timestamp: '2026-06-29T00:00:30.000Z',
321
535
  mode: 'executor',
322
536
  role: 'executor',
537
+ provenanceKind: NATIVE_SUBAGENT_PROVENANCE,
323
538
  laneId: 'implementation-fix',
324
539
  scope: 'runtime hook guard',
325
540
  agentNickname: 'worker-1',
@@ -343,6 +558,7 @@ describe('subagents/tracker', () => {
343
558
  ]);
344
559
  assert.equal(state.sessions['sess-conductor']?.threads['thread-executor']?.role, 'executor');
345
560
  assert.equal(state.sessions['sess-conductor']?.threads['thread-executor']?.lane_id, 'implementation-fix');
561
+ assert.equal(state.sessions['sess-conductor']?.threads['thread-executor']?.provenance_kind, NATIVE_SUBAGENT_PROVENANCE);
346
562
  });
347
563
  it('builds a reusable ledger that preserves unavailable status and handoff summaries', () => {
348
564
  let state = createSubagentTrackingState();
@@ -488,15 +704,1339 @@ describe('subagents/tracker', () => {
488
704
  });
489
705
  assert.ok(ledger);
490
706
  assert.equal(ledger.savedSubagents.find((entry) => entry.agentId === 'thread-closed-recent')?.status, 'closed');
491
- assert.deepEqual(ledger.resumeTargets.map((entry) => entry.agentId), [
492
- 'thread-available-older',
493
- 'thread-closed-recent',
494
- ]);
707
+ assert.deepEqual(ledger.resumeTargets.map((entry) => entry.agentId), ['thread-available-older', 'thread-closed-recent']);
495
708
  assert.equal(selectReusableSubagentEntry(ledger.resumeTargets, {
496
709
  role: 'executor',
497
710
  laneId: 'implementation-fix',
498
711
  scope: 'conductor reuse ledger',
499
712
  })?.agentId, 'thread-available-older');
500
713
  });
714
+ it('does not let a stale owner release a successor lock', async () => {
715
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
716
+ const resourcePath = join(cwd, 'lock-resource');
717
+ const lockPath = crossProcessLockPath(resourcePath);
718
+ const successorReadyPath = join(cwd, 'successor-ready');
719
+ const successorReleasePath = join(cwd, 'successor-release');
720
+ let successor;
721
+ try {
722
+ withCrossProcessFileLockSync(resourcePath, () => {
723
+ writeFileSync(lockPath, crossProcessLockClaim('expired-owner-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
724
+ successor = spawnCrossProcessLockHolder(resourcePath, successorReadyPath, successorReleasePath);
725
+ waitForFileSync(successorReadyPath);
726
+ });
727
+ assert.ok(successor);
728
+ const successorClaim = JSON.parse(readFileSync(lockPath, 'utf-8'));
729
+ assert.equal(successorClaim.pid, successor.pid);
730
+ assert.notEqual(successorClaim.token, 'expired-owner-token');
731
+ assert.ok(existsSync(lockPath));
732
+ writeFileSync(successorReleasePath, 'release\n');
733
+ await waitForChildExit(successor);
734
+ assert.equal(existsSync(lockPath), false);
735
+ }
736
+ finally {
737
+ await stopChild(successor);
738
+ await rm(cwd, { recursive: true, force: true });
739
+ }
740
+ });
741
+ it('removes displaced quarantine and release artifacts when a replacement wins restoration', async () => {
742
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
743
+ const resourcePath = join(cwd, 'lock-resource');
744
+ const lockPath = crossProcessLockPath(resourcePath);
745
+ try {
746
+ writeFileSync(lockPath, crossProcessLockClaim('stale-remote-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
747
+ __setCrossProcessQuarantineBarrierForTest((replacementPath, quarantinedPath) => {
748
+ writeFileSync(quarantinedPath, crossProcessLockClaim('fresh-replacement-token', process.pid, Date.now()));
749
+ writeFileSync(replacementPath, crossProcessLockClaim('fresh-successor-token', process.pid, Date.now()));
750
+ });
751
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, () => 'must not acquire', { maxAttempts: 1, retryMs: 1 }), /Timed out waiting for cross-process lock/);
752
+ assert.deepEqual(readdirSync(cwd).filter((entry) => entry.endsWith('.quarantine') || entry.endsWith('.release')), []);
753
+ await rm(lockPath, { force: true });
754
+ __setCrossProcessQuarantineBarrierForTest((replacementPath, quarantinedPath) => {
755
+ writeFileSync(quarantinedPath, crossProcessLockClaim('release-replacement-token', process.pid, Date.now()));
756
+ writeFileSync(replacementPath, crossProcessLockClaim('release-successor-token', process.pid, Date.now()));
757
+ });
758
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'released'), 'released');
759
+ assert.deepEqual(readdirSync(cwd).filter((entry) => entry.endsWith('.quarantine') || entry.endsWith('.release')), []);
760
+ }
761
+ finally {
762
+ __setCrossProcessQuarantineBarrierForTest(null);
763
+ await rm(cwd, { recursive: true, force: true });
764
+ }
765
+ });
766
+ it('sweeps only lease-aged parseable lock displacement artifacts', async () => {
767
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
768
+ const resourcePath = join(cwd, 'lock-resource');
769
+ const lockPath = crossProcessLockPath(resourcePath);
770
+ const oldQuarantinePath = `${lockPath}.${Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1}.fixture.quarantine`;
771
+ const freshReleasePath = `${lockPath}.${Date.now()}.fixture.release`;
772
+ const malformedTimestampPath = `${lockPath}.notanumber.fixture.quarantine`;
773
+ try {
774
+ writeFileSync(oldQuarantinePath, 'old\n');
775
+ writeFileSync(freshReleasePath, 'fresh\n');
776
+ writeFileSync(malformedTimestampPath, 'malformed\n');
777
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'swept'), 'swept');
778
+ assert.equal(existsSync(oldQuarantinePath), false);
779
+ assert.equal(existsSync(freshReleasePath), true);
780
+ assert.equal(existsSync(malformedTimestampPath), true);
781
+ }
782
+ finally {
783
+ await rm(cwd, { recursive: true, force: true });
784
+ }
785
+ });
786
+ it('bounds the displacement-artifact sweep by a fixed per-acquisition cap, oldest-first', async () => {
787
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
788
+ const resourcePath = join(cwd, 'lock-resource');
789
+ const lockPath = crossProcessLockPath(resourcePath);
790
+ const overflow = 5;
791
+ const total = CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP + overflow;
792
+ const base = Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - total - 1;
793
+ const artifactPaths = [];
794
+ try {
795
+ for (let i = 0; i < total; i += 1) {
796
+ // Distinct ascending timestamps so oldest-first ordering is deterministic.
797
+ const path = `${lockPath}.${base + i}.fixture-${String(i).padStart(4, '0')}.quarantine`;
798
+ writeFileSync(path, `artifact-${i}\n`);
799
+ artifactPaths.push(path);
800
+ }
801
+ // First acquisition removes exactly CAP oldest artifacts (bounded work).
802
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'first'), 'first');
803
+ const remainingAfterFirst = artifactPaths.filter((path) => existsSync(path));
804
+ assert.equal(remainingAfterFirst.length, overflow);
805
+ // Survivors are the newest ones (deterministic oldest-first cap).
806
+ assert.deepEqual(remainingAfterFirst, artifactPaths.slice(CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP));
807
+ // A subsequent acquisition drains the remaining backlog.
808
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'second'), 'second');
809
+ assert.deepEqual(artifactPaths.filter((path) => existsSync(path)), []);
810
+ }
811
+ finally {
812
+ await rm(cwd, { recursive: true, force: true });
813
+ }
814
+ });
815
+ it('treats an already-removed displaced release artifact as a clean terminal cleanup', async () => {
816
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
817
+ const resourcePath = join(cwd, 'lock-resource');
818
+ try {
819
+ // Simulate a concurrent bounded sweep unlinking the in-flight displaced .release
820
+ // between the release rename and its restore attempt: cleanup must fence to a clean
821
+ // terminal outcome (never throw from cleanup after a successful publication).
822
+ __setCrossProcessQuarantineBarrierForTest((_lockPath, quarantinedPath) => {
823
+ rmSync(quarantinedPath, { force: true });
824
+ });
825
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'published'), 'published');
826
+ assert.equal(existsSync(crossProcessLockPath(resourcePath)), false);
827
+ }
828
+ finally {
829
+ __setCrossProcessQuarantineBarrierForTest(null);
830
+ await rm(cwd, { recursive: true, force: true });
831
+ }
832
+ });
833
+ it('reclaims a dead-owner cross-process lock', async () => {
834
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
835
+ const resourcePath = join(cwd, 'lock-resource');
836
+ try {
837
+ const child = spawn(process.execPath, ['--eval', '']);
838
+ const deadOwnerPid = child.pid;
839
+ await waitForChildExit(child);
840
+ assert.ok(deadOwnerPid);
841
+ await writeFile(crossProcessLockPath(resourcePath), crossProcessLockClaim('dead-owner-token', deadOwnerPid, Date.now()));
842
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'dead lock recovered'), 'dead lock recovered');
843
+ assert.equal(existsSync(crossProcessLockPath(resourcePath)), false);
844
+ }
845
+ finally {
846
+ await rm(cwd, { recursive: true, force: true });
847
+ }
848
+ });
849
+ it('does not steal a live different-pid lock within its lease', async () => {
850
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
851
+ const resourcePath = join(cwd, 'lock-resource');
852
+ const lockPath = crossProcessLockPath(resourcePath);
853
+ const liveOwner = spawn(process.execPath, ['--eval', 'setInterval(() => {}, 1_000)']);
854
+ let operationRan = false;
855
+ try {
856
+ assert.ok(liveOwner.pid);
857
+ const claim = crossProcessLockClaim('live-owner-token', liveOwner.pid, Date.now());
858
+ await writeFile(lockPath, claim);
859
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, () => {
860
+ operationRan = true;
861
+ }, { maxAttempts: 2, retryMs: 1 }), /Timed out waiting for cross-process lock/);
862
+ assert.equal(operationRan, false);
863
+ assert.equal(readFileSync(lockPath, 'utf-8'), claim);
864
+ }
865
+ finally {
866
+ await stopChild(liveOwner);
867
+ await rm(cwd, { recursive: true, force: true });
868
+ }
869
+ });
870
+ it('fences a stalled claimant after a remote-lease successor publishes', async () => {
871
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
872
+ const resourcePath = join(cwd, 'lock-resource');
873
+ const lockPath = crossProcessLockPath(resourcePath);
874
+ const successorReadyPath = join(cwd, 'successor-ready');
875
+ const successorReleasePath = join(cwd, 'successor-release');
876
+ const publicationPath = join(cwd, 'publication');
877
+ let successor;
878
+ try {
879
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, (context) => {
880
+ writeFileSync(lockPath, crossProcessLockClaim('stalled-remote-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
881
+ successor = spawnCrossProcessLockHolder(resourcePath, successorReadyPath, successorReleasePath, publicationPath);
882
+ waitForFileSync(successorReadyPath);
883
+ context.assertOwnership();
884
+ writeFileSync(publicationPath, 'stale predecessor\n');
885
+ }), CrossProcessLockLostError);
886
+ assert.ok(successor);
887
+ assert.equal(readFileSync(publicationPath, 'utf-8'), 'successor\n');
888
+ assert.ok(existsSync(lockPath));
889
+ }
890
+ finally {
891
+ if (successor) {
892
+ writeFileSync(successorReleasePath, 'release\n');
893
+ await waitForChildExit(successor);
894
+ }
895
+ await rm(cwd, { recursive: true, force: true });
896
+ }
897
+ });
898
+ it('prevents a fenced predecessor from publishing after its staged slot is swept', async () => {
899
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
900
+ const resourcePath = join(cwd, 'lock-resource');
901
+ const lockPath = crossProcessLockPath(resourcePath);
902
+ let barrierRuns = 0;
903
+ try {
904
+ __setCrossProcessPublishBarrierForTest(() => {
905
+ barrierRuns += 1;
906
+ writeFileSync(lockPath, crossProcessLockClaim('stalled-remote-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
907
+ withCrossProcessFileLockSync(resourcePath, (context) => {
908
+ context.publish('S_B');
909
+ }, { maxAttempts: 2, retryMs: 1 });
910
+ });
911
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, (context) => {
912
+ context.publish('S_A');
913
+ }), CrossProcessLockLostError);
914
+ assert.equal(barrierRuns, 1);
915
+ assert.equal(readFileSync(resourcePath, 'utf-8'), 'S_B');
916
+ }
917
+ finally {
918
+ __setCrossProcessPublishBarrierForTest(null);
919
+ await rm(cwd, { recursive: true, force: true });
920
+ }
921
+ });
922
+ it('reports lock loss when a successor sweeps a staged slot before publication opens it', async () => {
923
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
924
+ const resourcePath = join(cwd, 'lock-resource');
925
+ const lockPath = crossProcessLockPath(resourcePath);
926
+ try {
927
+ withCrossProcessFileLockSync(resourcePath, (context) => {
928
+ writeFileSync(lockPath, crossProcessLockClaim('stalled-remote-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
929
+ withCrossProcessFileLockSync(resourcePath, (successor) => {
930
+ successor.publish('S_B');
931
+ }, { maxAttempts: 2, retryMs: 1 });
932
+ assert.throws(() => context.publish('S_A'), CrossProcessLockLostError);
933
+ });
934
+ assert.equal(readFileSync(resourcePath, 'utf-8'), 'S_B');
935
+ }
936
+ finally {
937
+ await rm(cwd, { recursive: true, force: true });
938
+ }
939
+ });
940
+ it('recovers despite an abandoned legacy recovery guard', async () => {
941
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
942
+ const resourcePath = join(cwd, 'lock-resource');
943
+ const lockPath = crossProcessLockPath(resourcePath);
944
+ try {
945
+ await mkdir(`${lockPath}.guard`);
946
+ await writeFile(lockPath, '');
947
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'guard ignored'), 'guard ignored');
948
+ assert.equal(existsSync(lockPath), false);
949
+ }
950
+ finally {
951
+ await rm(cwd, { recursive: true, force: true });
952
+ }
953
+ });
954
+ it('does not use local pid liveness to reclaim a remote claim before its lease', async () => {
955
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
956
+ const resourcePath = join(cwd, 'lock-resource');
957
+ const lockPath = crossProcessLockPath(resourcePath);
958
+ const deadOwner = spawn(process.execPath, ['--eval', '']);
959
+ try {
960
+ const deadOwnerPid = deadOwner.pid;
961
+ await waitForChildExit(deadOwner);
962
+ assert.ok(deadOwnerPid);
963
+ const freshRemoteClaim = crossProcessLockClaim('fresh-remote-token', deadOwnerPid, Date.now(), 'remote-test-host');
964
+ await writeFile(lockPath, freshRemoteClaim);
965
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, () => 'must not run', { maxAttempts: 2, retryMs: 1 }), /Timed out waiting for cross-process lock/);
966
+ assert.equal(readFileSync(lockPath, 'utf-8'), freshRemoteClaim);
967
+ await writeFile(lockPath, crossProcessLockClaim('expired-remote-token', deadOwnerPid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
968
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'remote lease recovered'), 'remote lease recovered');
969
+ }
970
+ finally {
971
+ await rm(cwd, { recursive: true, force: true });
972
+ }
973
+ });
974
+ it('reclaims a same-host claim held by a reused live pid', async () => {
975
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
976
+ const resourcePath = join(cwd, 'lock-resource');
977
+ const lockPath = crossProcessLockPath(resourcePath);
978
+ const pidStartId = readProcessStartIdentity(process.pid);
979
+ try {
980
+ if (!pidStartId)
981
+ return;
982
+ await writeFile(lockPath, crossProcessLockClaim('reused-pid-token', process.pid, Date.now(), hostname(), 'bogus-pid-start-id'));
983
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'reused pid recovered', { maxAttempts: 1, retryMs: 1 }), 'reused pid recovered');
984
+ }
985
+ finally {
986
+ await rm(cwd, { recursive: true, force: true });
987
+ }
988
+ });
989
+ it('uses the lease fallback for legacy same-host claims without a process identity', async () => {
990
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
991
+ const resourcePath = join(cwd, 'lock-resource');
992
+ const lockPath = crossProcessLockPath(resourcePath);
993
+ try {
994
+ await writeFile(lockPath, crossProcessLockClaim('expired-legacy-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1));
995
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'legacy lease recovered', { maxAttempts: 1, retryMs: 1 }), 'legacy lease recovered');
996
+ const freshLegacyClaim = crossProcessLockClaim('fresh-legacy-token', process.pid, Date.now());
997
+ await writeFile(lockPath, freshLegacyClaim);
998
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, () => 'must not run', { maxAttempts: 2, retryMs: 1 }), /Timed out waiting for cross-process lock/);
999
+ assert.equal(readFileSync(lockPath, 'utf-8'), freshLegacyClaim);
1000
+ }
1001
+ finally {
1002
+ await rm(cwd, { recursive: true, force: true });
1003
+ }
1004
+ });
1005
+ it('does not steal a live same-host identity-matched claim after its lease or mistake its token for ours', async () => {
1006
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1007
+ const resourcePath = join(cwd, 'lock-resource');
1008
+ const lockPath = crossProcessLockPath(resourcePath);
1009
+ const pidStartId = readProcessStartIdentity(process.pid);
1010
+ try {
1011
+ if (!pidStartId)
1012
+ return;
1013
+ const reusedPidClaim = crossProcessLockClaim('reused-pid-token', process.pid, Date.now() - CROSS_PROCESS_LOCK_LEASE_MS - 1, hostname(), pidStartId);
1014
+ await writeFile(lockPath, reusedPidClaim);
1015
+ assert.throws(() => withCrossProcessFileLockSync(resourcePath, () => 'must not run', { maxAttempts: 2, retryMs: 1 }), /Timed out waiting for cross-process lock/);
1016
+ assert.equal(readFileSync(lockPath, 'utf-8'), reusedPidClaim);
1017
+ }
1018
+ finally {
1019
+ await rm(cwd, { recursive: true, force: true });
1020
+ }
1021
+ });
1022
+ it('recovers empty, partial, and malformed cross-process lock claims', async () => {
1023
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1024
+ const resourcePath = join(cwd, 'lock-resource');
1025
+ const lockPath = crossProcessLockPath(resourcePath);
1026
+ try {
1027
+ for (const malformedLock of ['', '{"token":"partial"', 'not valid json\n']) {
1028
+ await writeFile(lockPath, malformedLock);
1029
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'malformed lock recovered', { maxAttempts: 1, retryMs: 1 }), 'malformed lock recovered');
1030
+ assert.equal(existsSync(lockPath), false);
1031
+ }
1032
+ }
1033
+ finally {
1034
+ await rm(cwd, { recursive: true, force: true });
1035
+ }
1036
+ });
1037
+ it('uses the shared recoverable lock for role-routing markers', async () => {
1038
+ const baseStateDir = await mkdtemp(join(tmpdir(), 'omx-role-routing-marker-'));
1039
+ const nowMs = Date.now();
1040
+ try {
1041
+ await writeFile(crossProcessLockPath(join(baseStateDir, NATIVE_SUBAGENT_ROLE_ROUTING_MARKER_FILE)), crossProcessLockClaim('expired-marker-token', process.pid, nowMs - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
1042
+ writeRoleRoutingMarker(baseStateDir, {
1043
+ schema_version: 1,
1044
+ cwd: '/workspace/project',
1045
+ session_id: 'sess-role-routing-marker',
1046
+ parent_thread_id: 'thread-parent',
1047
+ observed_at: new Date(nowMs).toISOString(),
1048
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1049
+ });
1050
+ assert.equal(readRoleRoutingMarker(baseStateDir, {
1051
+ cwd: '/workspace/project',
1052
+ sessionId: 'sess-role-routing-marker',
1053
+ parentThreadId: 'thread-parent',
1054
+ nowMs,
1055
+ })?.session_id, 'sess-role-routing-marker');
1056
+ }
1057
+ finally {
1058
+ await rm(baseStateDir, { recursive: true, force: true });
1059
+ }
1060
+ });
1061
+ it('reclaims a same-host claim after a reboot changes the boot id', async () => {
1062
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1063
+ const resourcePath = join(cwd, 'lock-resource');
1064
+ const lockPath = crossProcessLockPath(resourcePath);
1065
+ const pidStartId = readProcessStartIdentity(process.pid);
1066
+ try {
1067
+ if (!pidStartId)
1068
+ return;
1069
+ const starttime = pidStartId.slice(pidStartId.indexOf(':') + 1);
1070
+ const rebootedClaim = crossProcessLockClaim('pre-reboot-token', process.pid, Date.now(), hostname(), `pre-reboot-boot-id:${starttime}`);
1071
+ await writeFile(lockPath, rebootedClaim);
1072
+ assert.equal(withCrossProcessFileLockSync(resourcePath, () => 'reboot recovered', { maxAttempts: 1, retryMs: 1 }), 'reboot recovered');
1073
+ }
1074
+ finally {
1075
+ await rm(cwd, { recursive: true, force: true });
1076
+ }
1077
+ });
1078
+ it('fences a stalled role-routing marker writer after a successor publishes and leaves no temp artifacts', async () => {
1079
+ const baseStateDir = await mkdtemp(join(tmpdir(), 'omx-role-routing-marker-'));
1080
+ const markerPath = join(baseStateDir, NATIVE_SUBAGENT_ROLE_ROUTING_MARKER_FILE);
1081
+ const lockPath = crossProcessLockPath(markerPath);
1082
+ const nowMs = Date.now();
1083
+ let barrierRuns = 0;
1084
+ const markerFor = (sessionId) => ({
1085
+ schema_version: 1,
1086
+ cwd: '/workspace/project',
1087
+ session_id: sessionId,
1088
+ parent_thread_id: 'thread-parent',
1089
+ observed_at: new Date(nowMs).toISOString(),
1090
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1091
+ });
1092
+ try {
1093
+ __setCrossProcessPublishBarrierForTest(() => {
1094
+ barrierRuns += 1;
1095
+ writeFileSync(lockPath, crossProcessLockClaim('stalled-marker-token', process.pid, nowMs - CROSS_PROCESS_LOCK_LEASE_MS - 1, 'remote-test-host'));
1096
+ writeRoleRoutingMarker(baseStateDir, markerFor('sess-successor'));
1097
+ });
1098
+ assert.throws(() => writeRoleRoutingMarker(baseStateDir, markerFor('sess-stalled')), CrossProcessLockLostError);
1099
+ assert.equal(barrierRuns, 1);
1100
+ assert.equal(readRoleRoutingMarker(baseStateDir, { cwd: '/workspace/project', sessionId: 'sess-successor', parentThreadId: 'thread-parent', nowMs })?.session_id, 'sess-successor');
1101
+ assert.equal(readRoleRoutingMarker(baseStateDir, { cwd: '/workspace/project', sessionId: 'sess-stalled', parentThreadId: 'thread-parent', nowMs }), null);
1102
+ assert.deepEqual(readdirSync(baseStateDir).filter((entry) => entry.includes('.stage.') || entry.endsWith('.tmp')), []);
1103
+ }
1104
+ finally {
1105
+ __setCrossProcessPublishBarrierForTest(null);
1106
+ await rm(baseStateDir, { recursive: true, force: true });
1107
+ }
1108
+ });
1109
+ it('rejects pending role intents for unknown roles', () => {
1110
+ assert.deepEqual(recordPendingRoleIntent(process.cwd(), {
1111
+ role: 'not-a-known-role',
1112
+ sessionId: 'sess-role-intent',
1113
+ parentThreadId: 'thread-parent',
1114
+ correlationToken: canonicalCorrelationToken('correlation-token'),
1115
+ }), { ok: false, reason: 'unknown_role' });
1116
+ });
1117
+ it('rejects pending role intents with an empty correlation token', () => {
1118
+ assert.deepEqual(recordPendingRoleIntent(process.cwd(), {
1119
+ role: 'architect',
1120
+ sessionId: 'sess-role-intent',
1121
+ parentThreadId: 'thread-parent',
1122
+ correlationToken: '',
1123
+ }), { ok: false, reason: 'invalid_correlation_token' });
1124
+ });
1125
+ it('rejects parser-invalid tokens before persistence and round-trips valid tokens', async () => {
1126
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-token-grammar-'));
1127
+ try {
1128
+ assert.deepEqual(recordPendingRoleIntent(cwd, {
1129
+ role: 'architect',
1130
+ sessionId: 'session-token-grammar',
1131
+ parentThreadId: 'parent-token-grammar',
1132
+ correlationToken: 'abc_def',
1133
+ }), { ok: false, reason: 'invalid_correlation_token' });
1134
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 0);
1135
+ assert.equal(recordPendingRoleIntent(cwd, {
1136
+ role: 'architect',
1137
+ sessionId: 'session-token-grammar',
1138
+ parentThreadId: 'parent-token-grammar',
1139
+ correlationToken: canonicalCorrelationToken('abc123'),
1140
+ }).ok, true);
1141
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents[0]?.correlation_token, canonicalCorrelationToken('abc123'));
1142
+ }
1143
+ finally {
1144
+ await rm(cwd, { recursive: true, force: true });
1145
+ }
1146
+ });
1147
+ it('rejects a second live role intent for the same parent thread', async () => {
1148
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1149
+ try {
1150
+ assert.equal(recordPendingRoleIntent(cwd, {
1151
+ role: 'architect',
1152
+ sessionId: 'sess-role-intent',
1153
+ parentThreadId: 'thread-parent',
1154
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1155
+ nowMs: 1_000,
1156
+ }).ok, true);
1157
+ assert.deepEqual(recordPendingRoleIntent(cwd, {
1158
+ role: 'critic',
1159
+ sessionId: 'sess-role-intent',
1160
+ parentThreadId: 'thread-parent',
1161
+ correlationToken: canonicalCorrelationToken('critictoken'),
1162
+ nowMs: 1_001,
1163
+ }), { ok: false, reason: 'single_flight_conflict' });
1164
+ }
1165
+ finally {
1166
+ await rm(cwd, { recursive: true, force: true });
1167
+ }
1168
+ });
1169
+ it('serializes concurrent pending role intent records across processes', async () => {
1170
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1171
+ try {
1172
+ const results = await runConcurrentPendingRoleIntentWorkers(cwd, [
1173
+ {
1174
+ operation: 'record',
1175
+ cwd,
1176
+ role: 'architect',
1177
+ sessionId: 'sess-role-intent-concurrent-record',
1178
+ parentThreadId: 'thread-parent',
1179
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1180
+ nowMs: 1_000,
1181
+ },
1182
+ {
1183
+ operation: 'record',
1184
+ cwd,
1185
+ role: 'critic',
1186
+ sessionId: 'sess-role-intent-concurrent-record',
1187
+ parentThreadId: 'thread-parent',
1188
+ correlationToken: canonicalCorrelationToken('critictoken'),
1189
+ nowMs: 1_000,
1190
+ },
1191
+ ]);
1192
+ assert.equal(results.filter(isSuccessfulRoleIntentRecord).length, 1);
1193
+ assert.equal(results.filter(isSingleFlightConflict).length, 1);
1194
+ }
1195
+ finally {
1196
+ await rm(cwd, { recursive: true, force: true });
1197
+ }
1198
+ });
1199
+ it('preserves expired foreign-origin journals through successful pending-role lifecycle writes', async () => {
1200
+ const sharedRoot = await mkdtemp(join(tmpdir(), 'omx-tracker-shared-writeback-'));
1201
+ const cwdA = join(sharedRoot, 'workspace-a');
1202
+ const cwdB = join(sharedRoot, 'workspace-b');
1203
+ const previousStateRoot = process.env.OMX_STATE_ROOT;
1204
+ try {
1205
+ process.env.OMX_STATE_ROOT = sharedRoot;
1206
+ await mkdir(cwdA, { recursive: true });
1207
+ await mkdir(cwdB, { recursive: true });
1208
+ assert.equal(getBaseStateDir(cwdA), getBaseStateDir(cwdB));
1209
+ const foreignResult = recordPendingRoleIntent(cwdA, {
1210
+ role: 'architect',
1211
+ sessionId: 'session-foreign',
1212
+ parentThreadId: 'parent-foreign',
1213
+ correlationToken: canonicalCorrelationToken('tokenforeign'),
1214
+ ttlMs: 1,
1215
+ nowMs: 1_000,
1216
+ });
1217
+ assert.equal(foreignResult.ok, true);
1218
+ if (!foreignResult.ok)
1219
+ throw new Error('Expected the foreign role intent to record.');
1220
+ const foreignIntent = foreignResult.intent;
1221
+ const assertForeignRetained = async () => {
1222
+ const retained = (await readSubagentTrackingState(cwdB)).pending_role_intents
1223
+ .find((intent) => intent.correlation_token === foreignIntent.correlation_token);
1224
+ assert.deepEqual(retained, foreignIntent);
1225
+ };
1226
+ const nowMs = 1_002;
1227
+ assert.equal(recordPendingRoleIntent(cwdB, {
1228
+ role: 'critic',
1229
+ sessionId: 'session-bound',
1230
+ parentThreadId: 'parent-bound',
1231
+ correlationToken: canonicalCorrelationToken('tokenbound'),
1232
+ nowMs,
1233
+ }).ok, true);
1234
+ await assertForeignRetained();
1235
+ const binding = bindPendingRoleIntentUnderLock(cwdB, {
1236
+ sessionId: 'session-bound',
1237
+ parentThreadId: 'parent-bound',
1238
+ correlationToken: canonicalCorrelationToken('tokenbound'),
1239
+ nowMs,
1240
+ }, (state) => state);
1241
+ assert.ok(binding?.claimantToken);
1242
+ await assertForeignRetained();
1243
+ assert.equal(completeAdaptedRoleBinding(cwdB, {
1244
+ sessionId: 'session-bound',
1245
+ parentThreadId: 'parent-bound',
1246
+ correlationToken: canonicalCorrelationToken('tokenbound'),
1247
+ claimantToken: binding?.claimantToken,
1248
+ nowMs,
1249
+ }), 'completed');
1250
+ await assertForeignRetained();
1251
+ assert.equal(recordPendingRoleIntent(cwdB, {
1252
+ role: 'critic',
1253
+ sessionId: 'session-consume',
1254
+ parentThreadId: 'parent-consume',
1255
+ correlationToken: canonicalCorrelationToken('tokenconsume'),
1256
+ nowMs,
1257
+ }).ok, true);
1258
+ assert.deepEqual(consumePendingRoleIntent(cwdB, {
1259
+ sessionId: 'session-consume',
1260
+ parentThreadId: 'parent-consume',
1261
+ correlationToken: canonicalCorrelationToken('tokenconsume'),
1262
+ nowMs,
1263
+ }), { role: 'critic', provenanceKind: OMX_ADAPTED_PROVENANCE });
1264
+ await assertForeignRetained();
1265
+ }
1266
+ finally {
1267
+ if (previousStateRoot === undefined)
1268
+ delete process.env.OMX_STATE_ROOT;
1269
+ else
1270
+ process.env.OMX_STATE_ROOT = previousStateRoot;
1271
+ await rm(sharedRoot, { recursive: true, force: true });
1272
+ }
1273
+ });
1274
+ it('binds a pending role intent only when its correlation token matches', async () => {
1275
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1276
+ const input = {
1277
+ sessionId: 'sess-role-intent',
1278
+ parentThreadId: 'thread-parent',
1279
+ nowMs: 1_001,
1280
+ };
1281
+ let bindCount = 0;
1282
+ const bind = (state, intent) => {
1283
+ bindCount += 1;
1284
+ assert.deepEqual(intent, { role: 'architect', provenanceKind: OMX_ADAPTED_PROVENANCE });
1285
+ return state;
1286
+ };
1287
+ try {
1288
+ assert.equal(recordPendingRoleIntent(cwd, {
1289
+ role: 'architect',
1290
+ sessionId: input.sessionId,
1291
+ parentThreadId: input.parentThreadId,
1292
+ correlationToken: canonicalCorrelationToken('expectedtoken'),
1293
+ nowMs: 1_000,
1294
+ }).ok, true);
1295
+ assert.equal(bindPendingRoleIntentUnderLock(cwd, input, bind), null);
1296
+ assert.equal(bindPendingRoleIntentUnderLock(cwd, { ...input, correlationToken: canonicalCorrelationToken('wrongtoken') }, bind), null);
1297
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents[0]?.correlation_token, canonicalCorrelationToken('expectedtoken'));
1298
+ const firstBinding = bindPendingRoleIntentUnderLock(cwd, { ...input, correlationToken: canonicalCorrelationToken('expectedtoken') }, bind);
1299
+ assert.equal(firstBinding?.role, 'architect');
1300
+ assert.equal(firstBinding?.provenanceKind, OMX_ADAPTED_PROVENANCE);
1301
+ assert.equal(firstBinding?.alreadyBound, false);
1302
+ assert.ok(firstBinding?.claimantToken);
1303
+ const retainedIntent = (await readSubagentTrackingState(cwd)).pending_role_intents[0];
1304
+ assert.equal(retainedIntent?.binding_state, 'bound');
1305
+ assert.equal(retainedIntent?.binding_claimant_token, firstBinding?.claimantToken);
1306
+ const replay = bindPendingRoleIntentUnderLock(cwd, { ...input, correlationToken: canonicalCorrelationToken('expectedtoken') }, bind);
1307
+ assert.deepEqual(replay, {
1308
+ role: 'architect',
1309
+ provenanceKind: OMX_ADAPTED_PROVENANCE,
1310
+ claimantToken: undefined,
1311
+ alreadyBound: true,
1312
+ });
1313
+ assert.equal(bindCount, 1);
1314
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...input, correlationToken: canonicalCorrelationToken('expectedtoken'), claimantToken: replay?.claimantToken }), 'claimant_mismatch');
1315
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...input, correlationToken: canonicalCorrelationToken('expectedtoken'), claimantToken: firstBinding?.claimantToken }), 'completed');
1316
+ }
1317
+ finally {
1318
+ await rm(cwd, { recursive: true, force: true });
1319
+ }
1320
+ });
1321
+ it('migrates a same-workspace cwd-partitioned legacy journal on bind and still consumes one', async () => {
1322
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-legacy-'));
1323
+ const previousOmxRoot = process.env.OMX_ROOT;
1324
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1325
+ const previousStateRoot = process.env.OMX_STATE_ROOT;
1326
+ const nowMs = Date.now();
1327
+ try {
1328
+ delete process.env.OMX_ROOT;
1329
+ delete process.env.OMX_TEAM_STATE_ROOT;
1330
+ delete process.env.OMX_STATE_ROOT;
1331
+ await mkdir(getBaseStateDir(cwd), { recursive: true });
1332
+ const legacyIntent = {
1333
+ role: 'architect',
1334
+ session_id: 'legacy-session-bind',
1335
+ parent_thread_id: 'legacy-parent-bind',
1336
+ correlation_token: canonicalCorrelationToken('legacy-token-bind'),
1337
+ created_at: new Date(nowMs).toISOString(),
1338
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1339
+ };
1340
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1341
+ schemaVersion: 1,
1342
+ sessions: {},
1343
+ pending_role_intents: [legacyIntent],
1344
+ })}\n`);
1345
+ const binding = bindPendingRoleIntentUnderLock(cwd, {
1346
+ sessionId: legacyIntent.session_id,
1347
+ parentThreadId: legacyIntent.parent_thread_id,
1348
+ correlationToken: legacyIntent.correlation_token,
1349
+ nowMs,
1350
+ }, (state) => state);
1351
+ assert.equal(binding?.alreadyBound, false);
1352
+ assert.ok(binding?.claimantToken);
1353
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents[0]?.origin_cwd, canonicalizeOriginCwd(cwd));
1354
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1355
+ sessionId: legacyIntent.session_id,
1356
+ parentThreadId: legacyIntent.parent_thread_id,
1357
+ correlationToken: legacyIntent.correlation_token,
1358
+ claimantToken: binding?.claimantToken,
1359
+ nowMs,
1360
+ }), 'completed');
1361
+ const legacyConsumeIntent = {
1362
+ ...legacyIntent,
1363
+ session_id: 'legacy-session-consume',
1364
+ parent_thread_id: 'legacy-parent-consume',
1365
+ correlation_token: canonicalCorrelationToken('legacy-token-consume'),
1366
+ };
1367
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1368
+ schemaVersion: 1,
1369
+ sessions: {},
1370
+ pending_role_intents: [legacyConsumeIntent],
1371
+ })}\n`);
1372
+ assert.deepEqual(consumePendingRoleIntent(cwd, {
1373
+ sessionId: legacyConsumeIntent.session_id,
1374
+ parentThreadId: legacyConsumeIntent.parent_thread_id,
1375
+ correlationToken: legacyConsumeIntent.correlation_token,
1376
+ nowMs,
1377
+ }), { role: 'architect', provenanceKind: OMX_ADAPTED_PROVENANCE });
1378
+ }
1379
+ finally {
1380
+ if (previousOmxRoot === undefined)
1381
+ delete process.env.OMX_ROOT;
1382
+ else
1383
+ process.env.OMX_ROOT = previousOmxRoot;
1384
+ if (previousTeamStateRoot === undefined)
1385
+ delete process.env.OMX_TEAM_STATE_ROOT;
1386
+ else
1387
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1388
+ if (previousStateRoot === undefined)
1389
+ delete process.env.OMX_STATE_ROOT;
1390
+ else
1391
+ process.env.OMX_STATE_ROOT = previousStateRoot;
1392
+ await rm(cwd, { recursive: true, force: true });
1393
+ }
1394
+ });
1395
+ it('stamps an owned already-bound cwd-default legacy journal on bind replay without disclosing its claimant', async () => {
1396
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-bound-legacy-'));
1397
+ const previousOmxRoot = process.env.OMX_ROOT;
1398
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1399
+ const previousStateRoot = process.env.OMX_STATE_ROOT;
1400
+ const nowMs = Date.now();
1401
+ try {
1402
+ delete process.env.OMX_ROOT;
1403
+ delete process.env.OMX_TEAM_STATE_ROOT;
1404
+ delete process.env.OMX_STATE_ROOT;
1405
+ await mkdir(getBaseStateDir(cwd), { recursive: true });
1406
+ const legacyIntent = {
1407
+ role: 'architect',
1408
+ session_id: 'legacy-bound-session',
1409
+ parent_thread_id: 'legacy-bound-parent',
1410
+ correlation_token: canonicalCorrelationToken('a3118b'),
1411
+ created_at: new Date(nowMs).toISOString(),
1412
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1413
+ binding_state: 'bound',
1414
+ binding_claimant_token: canonicalClaimantToken('legacy-claimant'),
1415
+ bound_at: new Date(nowMs).toISOString(),
1416
+ };
1417
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1418
+ schemaVersion: 1,
1419
+ sessions: {},
1420
+ pending_role_intents: [legacyIntent],
1421
+ })}\n`);
1422
+ let bindCalled = false;
1423
+ assert.deepEqual(bindPendingRoleIntentUnderLock(cwd, {
1424
+ sessionId: legacyIntent.session_id,
1425
+ parentThreadId: legacyIntent.parent_thread_id,
1426
+ correlationToken: legacyIntent.correlation_token,
1427
+ nowMs,
1428
+ }, (state) => {
1429
+ bindCalled = true;
1430
+ return state;
1431
+ }), {
1432
+ role: 'architect',
1433
+ provenanceKind: OMX_ADAPTED_PROVENANCE,
1434
+ claimantToken: undefined,
1435
+ alreadyBound: true,
1436
+ });
1437
+ assert.equal(bindCalled, false);
1438
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents[0]?.origin_cwd, canonicalizeOriginCwd(cwd));
1439
+ }
1440
+ finally {
1441
+ if (previousOmxRoot === undefined)
1442
+ delete process.env.OMX_ROOT;
1443
+ else
1444
+ process.env.OMX_ROOT = previousOmxRoot;
1445
+ if (previousTeamStateRoot === undefined)
1446
+ delete process.env.OMX_TEAM_STATE_ROOT;
1447
+ else
1448
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1449
+ if (previousStateRoot === undefined)
1450
+ delete process.env.OMX_STATE_ROOT;
1451
+ else
1452
+ process.env.OMX_STATE_ROOT = previousStateRoot;
1453
+ await rm(cwd, { recursive: true, force: true });
1454
+ }
1455
+ });
1456
+ it('does not allow a shared-root workspace to claim another originless legacy journal', async () => {
1457
+ const sharedRoot = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-shared-legacy-'));
1458
+ const cwdA = join(sharedRoot, 'workspace-a');
1459
+ const cwdB = join(sharedRoot, 'workspace-b');
1460
+ const previousOmxRoot = process.env.OMX_ROOT;
1461
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1462
+ const previousStateRoot = process.env.OMX_STATE_ROOT;
1463
+ const nowMs = Date.now();
1464
+ try {
1465
+ delete process.env.OMX_ROOT;
1466
+ delete process.env.OMX_TEAM_STATE_ROOT;
1467
+ process.env.OMX_STATE_ROOT = sharedRoot;
1468
+ await mkdir(cwdA, { recursive: true });
1469
+ await mkdir(cwdB, { recursive: true });
1470
+ const legacyIntent = {
1471
+ role: 'architect',
1472
+ session_id: 'shared-legacy-session',
1473
+ parent_thread_id: 'shared-legacy-parent',
1474
+ correlation_token: canonicalCorrelationToken('shared-legacy-token'),
1475
+ created_at: new Date(nowMs).toISOString(),
1476
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1477
+ };
1478
+ await mkdir(getBaseStateDir(cwdA), { recursive: true });
1479
+ await writeFile(subagentTrackingPath(cwdA), `${JSON.stringify({
1480
+ schemaVersion: 1,
1481
+ sessions: {},
1482
+ pending_role_intents: [legacyIntent],
1483
+ })}\n`);
1484
+ let bindCalled = false;
1485
+ assert.equal(bindPendingRoleIntentUnderLock(cwdB, {
1486
+ sessionId: legacyIntent.session_id,
1487
+ parentThreadId: legacyIntent.parent_thread_id,
1488
+ correlationToken: legacyIntent.correlation_token,
1489
+ nowMs,
1490
+ }, (state) => {
1491
+ bindCalled = true;
1492
+ return state;
1493
+ }), null);
1494
+ assert.equal(bindCalled, false);
1495
+ assert.equal(consumePendingRoleIntent(cwdB, {
1496
+ sessionId: legacyIntent.session_id,
1497
+ parentThreadId: legacyIntent.parent_thread_id,
1498
+ correlationToken: legacyIntent.correlation_token,
1499
+ nowMs,
1500
+ }), null);
1501
+ assert.equal(completeAdaptedRoleBinding(cwdB, {
1502
+ sessionId: legacyIntent.session_id,
1503
+ parentThreadId: legacyIntent.parent_thread_id,
1504
+ correlationToken: legacyIntent.correlation_token,
1505
+ nowMs,
1506
+ }), 'not_found');
1507
+ assert.deepEqual((await readSubagentTrackingState(cwdA)).pending_role_intents, [legacyIntent]);
1508
+ }
1509
+ finally {
1510
+ if (previousOmxRoot === undefined)
1511
+ delete process.env.OMX_ROOT;
1512
+ else
1513
+ process.env.OMX_ROOT = previousOmxRoot;
1514
+ if (previousTeamStateRoot === undefined)
1515
+ delete process.env.OMX_TEAM_STATE_ROOT;
1516
+ else
1517
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1518
+ if (previousStateRoot === undefined)
1519
+ delete process.env.OMX_STATE_ROOT;
1520
+ else
1521
+ process.env.OMX_STATE_ROOT = previousStateRoot;
1522
+ await rm(sharedRoot, { recursive: true, force: true });
1523
+ }
1524
+ });
1525
+ it('selects exact-origin duplicate journals deterministically and consumes every own duplicate', async () => {
1526
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-duplicate-origin-'));
1527
+ const previousOmxRoot = process.env.OMX_ROOT;
1528
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1529
+ const previousStateRoot = process.env.OMX_STATE_ROOT;
1530
+ const nowMs = Date.now();
1531
+ const scope = {
1532
+ sessionId: 'duplicate-session',
1533
+ parentThreadId: 'duplicate-parent',
1534
+ correlationToken: canonicalCorrelationToken('a3118d'),
1535
+ };
1536
+ try {
1537
+ delete process.env.OMX_ROOT;
1538
+ delete process.env.OMX_TEAM_STATE_ROOT;
1539
+ delete process.env.OMX_STATE_ROOT;
1540
+ await mkdir(getBaseStateDir(cwd), { recursive: true });
1541
+ const journal = (role, originCwd, claimantToken) => ({
1542
+ role,
1543
+ session_id: scope.sessionId,
1544
+ parent_thread_id: scope.parentThreadId,
1545
+ correlation_token: scope.correlationToken,
1546
+ created_at: new Date(nowMs).toISOString(),
1547
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1548
+ ...(originCwd ? { origin_cwd: originCwd } : {}),
1549
+ ...(claimantToken ? {
1550
+ binding_state: 'bound',
1551
+ binding_claimant_token: canonicalClaimantToken(claimantToken),
1552
+ bound_at: new Date(nowMs).toISOString(),
1553
+ } : {}),
1554
+ });
1555
+ const legacy = journal('architect');
1556
+ const exact = journal('critic', cwd);
1557
+ const writeJournals = async (pendingRoleIntents) => {
1558
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1559
+ schemaVersion: 1,
1560
+ sessions: {},
1561
+ pending_role_intents: pendingRoleIntents,
1562
+ })}\n`);
1563
+ };
1564
+ for (const duplicateOrder of [[legacy, exact], [exact, legacy]]) {
1565
+ await writeJournals(duplicateOrder);
1566
+ assert.deepEqual(consumePendingRoleIntent(cwd, { ...scope, nowMs }), {
1567
+ role: 'critic',
1568
+ provenanceKind: OMX_ADAPTED_PROVENANCE,
1569
+ });
1570
+ assert.equal(consumePendingRoleIntent(cwd, { ...scope, nowMs }), null);
1571
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, []);
1572
+ }
1573
+ for (const duplicateOrder of [[legacy, exact], [exact, legacy]]) {
1574
+ await writeJournals(duplicateOrder);
1575
+ let boundRole;
1576
+ const binding = bindPendingRoleIntentUnderLock(cwd, { ...scope, nowMs }, (state, intent) => {
1577
+ boundRole = intent.role;
1578
+ return state;
1579
+ });
1580
+ assert.equal(binding?.role, 'critic');
1581
+ assert.equal(boundRole, 'critic');
1582
+ assert.ok(binding?.claimantToken);
1583
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents.map((intent) => intent.role), ['critic']);
1584
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1585
+ ...scope,
1586
+ claimantToken: binding?.claimantToken,
1587
+ nowMs,
1588
+ }), 'completed');
1589
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, []);
1590
+ }
1591
+ const legacyBound = journal('architect', undefined, 'legacyclaimant');
1592
+ const exactBound = journal('critic', cwd, 'exactclaimant');
1593
+ for (const duplicateOrder of [[legacyBound, exactBound], [exactBound, legacyBound]]) {
1594
+ await writeJournals(duplicateOrder);
1595
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1596
+ ...scope,
1597
+ claimantToken: canonicalClaimantToken('exactclaimant'),
1598
+ nowMs,
1599
+ }), 'completed');
1600
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, []);
1601
+ }
1602
+ const mixedStates = [
1603
+ {
1604
+ name: 'exact-bound-with-legacy-pending',
1605
+ bound: journal('critic', cwd, 'exact-bound-claimant'),
1606
+ pending: journal('architect'),
1607
+ },
1608
+ {
1609
+ name: 'legacy-bound-with-exact-pending',
1610
+ bound: journal('critic', undefined, 'legacy-bound-claimant'),
1611
+ pending: journal('architect', cwd),
1612
+ },
1613
+ ];
1614
+ for (const { name, bound, pending } of mixedStates) {
1615
+ for (const invocationOrder of [['bind', 'consume'], ['consume', 'bind']]) {
1616
+ for (const journalOrder of [[bound, pending], [pending, bound]]) {
1617
+ await writeJournals(journalOrder);
1618
+ const authorizedRoles = [];
1619
+ let bindCount = 0;
1620
+ for (const operation of invocationOrder) {
1621
+ if (operation === 'bind') {
1622
+ const binding = bindPendingRoleIntentUnderLock(cwd, { ...scope, nowMs }, (state) => {
1623
+ bindCount += 1;
1624
+ return state;
1625
+ });
1626
+ assert.deepEqual(binding, {
1627
+ role: 'critic',
1628
+ provenanceKind: OMX_ADAPTED_PROVENANCE,
1629
+ claimantToken: undefined,
1630
+ alreadyBound: true,
1631
+ }, `${name}:${invocationOrder.join('-')}:${journalOrder[0]?.role}`);
1632
+ authorizedRoles.push(binding?.role ?? 'missing');
1633
+ }
1634
+ else {
1635
+ assert.equal(consumePendingRoleIntent(cwd, { ...scope, nowMs }), null, `${name}:${invocationOrder.join('-')}:${journalOrder[0]?.role}`);
1636
+ }
1637
+ }
1638
+ assert.deepEqual(authorizedRoles, ['critic']);
1639
+ assert.equal(bindCount, 0);
1640
+ const retained = (await readSubagentTrackingState(cwd)).pending_role_intents;
1641
+ assert.equal(retained.length, 1);
1642
+ assert.equal(retained[0]?.role, 'critic');
1643
+ assert.equal(retained[0]?.binding_state, 'bound');
1644
+ assert.equal(retained[0]?.correlation_token, scope.correlationToken);
1645
+ }
1646
+ }
1647
+ }
1648
+ }
1649
+ finally {
1650
+ if (previousOmxRoot === undefined)
1651
+ delete process.env.OMX_ROOT;
1652
+ else
1653
+ process.env.OMX_ROOT = previousOmxRoot;
1654
+ if (previousTeamStateRoot === undefined)
1655
+ delete process.env.OMX_TEAM_STATE_ROOT;
1656
+ else
1657
+ process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1658
+ if (previousStateRoot === undefined)
1659
+ delete process.env.OMX_STATE_ROOT;
1660
+ else
1661
+ process.env.OMX_STATE_ROOT = previousStateRoot;
1662
+ await rm(cwd, { recursive: true, force: true });
1663
+ }
1664
+ });
1665
+ it('consumes a pending role intent exactly once', async () => {
1666
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1667
+ try {
1668
+ assert.equal(recordPendingRoleIntent(cwd, {
1669
+ role: 'architect',
1670
+ sessionId: 'sess-role-intent',
1671
+ parentThreadId: 'thread-parent',
1672
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1673
+ nowMs: 1_000,
1674
+ }).ok, true);
1675
+ assert.deepEqual(consumePendingRoleIntent(cwd, {
1676
+ sessionId: 'sess-role-intent',
1677
+ parentThreadId: 'thread-parent',
1678
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1679
+ nowMs: 1_001,
1680
+ }), { role: 'architect', provenanceKind: OMX_ADAPTED_PROVENANCE });
1681
+ assert.equal(consumePendingRoleIntent(cwd, {
1682
+ sessionId: 'sess-role-intent',
1683
+ parentThreadId: 'thread-parent',
1684
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1685
+ nowMs: 1_002,
1686
+ }), null);
1687
+ }
1688
+ finally {
1689
+ await rm(cwd, { recursive: true, force: true });
1690
+ }
1691
+ });
1692
+ it('completes retained adapted bindings only for the claimant that began them', async () => {
1693
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1694
+ const input = {
1695
+ sessionId: 'sess-complete-binding',
1696
+ parentThreadId: 'thread-parent',
1697
+ correlationToken: canonicalCorrelationToken('completetoken'),
1698
+ nowMs: 1_001,
1699
+ };
1700
+ try {
1701
+ assert.equal(recordPendingRoleIntent(cwd, {
1702
+ role: 'architect',
1703
+ sessionId: input.sessionId,
1704
+ parentThreadId: input.parentThreadId,
1705
+ correlationToken: input.correlationToken,
1706
+ nowMs: 1_000,
1707
+ }).ok, true);
1708
+ const binding = bindPendingRoleIntentUnderLock(cwd, input, (state) => state);
1709
+ assert.ok(binding?.claimantToken);
1710
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...input, claimantToken: canonicalClaimantToken('wrong-claimant') }), 'claimant_mismatch');
1711
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 1);
1712
+ // Fail-closed: an OMITTED caller token must not complete a journal that carries a
1713
+ // stored claimant token (prevents unauthenticated completion / successor theft).
1714
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1715
+ sessionId: input.sessionId,
1716
+ parentThreadId: input.parentThreadId,
1717
+ correlationToken: input.correlationToken,
1718
+ }), 'claimant_mismatch');
1719
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 1);
1720
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...input, claimantToken: binding.claimantToken }), 'completed');
1721
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...input, claimantToken: binding.claimantToken }), 'not_found');
1722
+ }
1723
+ finally {
1724
+ await rm(cwd, { recursive: true, force: true });
1725
+ }
1726
+ });
1727
+ it('requires a claimant-less bound journal to receive its durable correlation token before completion', async () => {
1728
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-claimant-less-'));
1729
+ const nowMs = Date.now();
1730
+ const intent = {
1731
+ role: 'architect',
1732
+ session_id: 'claimant-less-session',
1733
+ parent_thread_id: 'claimant-less-parent',
1734
+ correlation_token: canonicalCorrelationToken('a3118f'),
1735
+ created_at: new Date(nowMs).toISOString(),
1736
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1737
+ binding_state: 'bound',
1738
+ bound_at: new Date(nowMs).toISOString(),
1739
+ origin_cwd: cwd,
1740
+ };
1741
+ try {
1742
+ await mkdir(getBaseStateDir(cwd), { recursive: true });
1743
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1744
+ schemaVersion: 1,
1745
+ sessions: {},
1746
+ pending_role_intents: [intent],
1747
+ })}\n`);
1748
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1749
+ sessionId: intent.session_id,
1750
+ parentThreadId: intent.parent_thread_id,
1751
+ nowMs,
1752
+ }), 'claimant_mismatch');
1753
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, [intent]);
1754
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1755
+ sessionId: intent.session_id,
1756
+ parentThreadId: intent.parent_thread_id,
1757
+ correlationToken: intent.correlation_token,
1758
+ nowMs,
1759
+ }), 'completed');
1760
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, []);
1761
+ }
1762
+ finally {
1763
+ await rm(cwd, { recursive: true, force: true });
1764
+ }
1765
+ });
1766
+ it('serializes concurrent pending role intent consumes across processes', async () => {
1767
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1768
+ try {
1769
+ assert.equal(recordPendingRoleIntent(cwd, {
1770
+ role: 'architect',
1771
+ sessionId: 'sess-role-intent-concurrent-consume',
1772
+ parentThreadId: 'thread-parent',
1773
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1774
+ nowMs: 1_000,
1775
+ }).ok, true);
1776
+ const results = await runConcurrentPendingRoleIntentWorkers(cwd, [
1777
+ {
1778
+ operation: 'consume',
1779
+ cwd,
1780
+ sessionId: 'sess-role-intent-concurrent-consume',
1781
+ parentThreadId: 'thread-parent',
1782
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1783
+ nowMs: 1_001,
1784
+ },
1785
+ {
1786
+ operation: 'consume',
1787
+ cwd,
1788
+ sessionId: 'sess-role-intent-concurrent-consume',
1789
+ parentThreadId: 'thread-parent',
1790
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1791
+ nowMs: 1_001,
1792
+ },
1793
+ ]);
1794
+ assert.equal(results.filter(isConsumedRoleIntent).length, 1);
1795
+ assert.equal(results.filter((result) => result === null).length, 1);
1796
+ }
1797
+ finally {
1798
+ await rm(cwd, { recursive: true, force: true });
1799
+ }
1800
+ });
1801
+ it('serializes lifecycle tracking writes with pending role intent records and consumes', async () => {
1802
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1803
+ const sessionId = 'sess-role-intent-lifecycle-race';
1804
+ const parentThreadId = 'thread-parent';
1805
+ const correlationToken = canonicalCorrelationToken('architecttoken');
1806
+ try {
1807
+ const recordResults = await runConcurrentPendingRoleIntentWorkers(cwd, [
1808
+ {
1809
+ operation: 'record',
1810
+ cwd,
1811
+ role: 'architect',
1812
+ sessionId,
1813
+ parentThreadId,
1814
+ correlationToken,
1815
+ nowMs: 1_000,
1816
+ },
1817
+ {
1818
+ operation: 'record-turn',
1819
+ cwd,
1820
+ sessionId,
1821
+ threadId: 'thread-lifecycle-before-consume',
1822
+ },
1823
+ ]);
1824
+ assert.equal(isSuccessfulRoleIntentRecord(recordResults[0]), true);
1825
+ assert.equal(isSuccessfulRoleIntentRecord(recordResults[1]), true);
1826
+ const recorded = await readSubagentTrackingState(cwd);
1827
+ assert.equal(recorded.pending_role_intents.length, 1);
1828
+ assert.equal(recorded.pending_role_intents[0]?.role, 'architect');
1829
+ assert.equal(recorded.sessions[sessionId]?.threads['thread-lifecycle-before-consume']?.kind, 'leader');
1830
+ const consumeResults = await runConcurrentPendingRoleIntentWorkers(cwd, [
1831
+ {
1832
+ operation: 'consume',
1833
+ cwd,
1834
+ sessionId,
1835
+ parentThreadId,
1836
+ correlationToken,
1837
+ nowMs: 1_001,
1838
+ },
1839
+ {
1840
+ operation: 'record-turn',
1841
+ cwd,
1842
+ sessionId,
1843
+ threadId: 'thread-lifecycle-after-consume',
1844
+ },
1845
+ ]);
1846
+ assert.equal(isConsumedRoleIntent(consumeResults[0]), true);
1847
+ assert.equal(isSuccessfulRoleIntentRecord(consumeResults[1]), true);
1848
+ const consumed = await readSubagentTrackingState(cwd);
1849
+ assert.deepEqual(consumed.pending_role_intents, []);
1850
+ assert.equal(consumed.sessions[sessionId]?.threads['thread-lifecycle-before-consume']?.kind, 'leader');
1851
+ assert.equal(consumed.sessions[sessionId]?.threads['thread-lifecycle-after-consume']?.kind, 'subagent');
1852
+ }
1853
+ finally {
1854
+ await rm(cwd, { recursive: true, force: true });
1855
+ }
1856
+ });
1857
+ it('does not consume expired pending role intents', async () => {
1858
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-'));
1859
+ try {
1860
+ assert.equal(recordPendingRoleIntent(cwd, {
1861
+ role: 'architect',
1862
+ sessionId: 'sess-role-intent',
1863
+ parentThreadId: 'thread-parent',
1864
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1865
+ nowMs: 1_000,
1866
+ ttlMs: 10,
1867
+ }).ok, true);
1868
+ assert.equal(consumePendingRoleIntent(cwd, {
1869
+ sessionId: 'sess-role-intent',
1870
+ parentThreadId: 'thread-parent',
1871
+ correlationToken: canonicalCorrelationToken('architecttoken'),
1872
+ nowMs: 1_010,
1873
+ }), null);
1874
+ }
1875
+ finally {
1876
+ await rm(cwd, { recursive: true, force: true });
1877
+ }
1878
+ });
1879
+ it('retains invalid durable credentials and rejects whitespace callers without fallthrough', async () => {
1880
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-credentials-'));
1881
+ const nowMs = Date.now();
1882
+ const scope = { session_id: 'credential-session', parent_thread_id: 'credential-parent' };
1883
+ const base = {
1884
+ role: 'architect',
1885
+ ...scope,
1886
+ created_at: new Date(nowMs).toISOString(),
1887
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1888
+ binding_state: 'bound',
1889
+ origin_cwd: cwd,
1890
+ };
1891
+ try {
1892
+ await mkdir(getBaseStateDir(cwd), { recursive: true });
1893
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1894
+ schemaVersion: 1,
1895
+ sessions: {},
1896
+ pending_role_intents: [
1897
+ { ...base, correlation_token: '', binding_claimant_token: canonicalClaimantToken('claimant') },
1898
+ { ...base, correlation_token: canonicalCorrelationToken('validtoken'), binding_claimant_token: canonicalClaimantToken('claimant') },
1899
+ ],
1900
+ })}\n`);
1901
+ const retained = (await readSubagentTrackingState(cwd)).pending_role_intents;
1902
+ assert.equal(retained[0]?.correlation_token, '');
1903
+ assert.equal(retained[0]?.binding_claimant_token, canonicalClaimantToken('claimant'));
1904
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1905
+ sessionId: scope.session_id, parentThreadId: scope.parent_thread_id,
1906
+ correlationToken: canonicalCorrelationToken('validtoken'), claimantToken: canonicalClaimantToken('claimant'), nowMs,
1907
+ }), 'claimant_mismatch');
1908
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 2);
1909
+ await writeFile(subagentTrackingPath(cwd), `${JSON.stringify({
1910
+ schemaVersion: 1, sessions: {}, pending_role_intents: [{
1911
+ ...base, correlation_token: canonicalCorrelationToken('validtoken'), binding_claimant_token: canonicalClaimantToken('claimant'),
1912
+ }],
1913
+ })}\n`);
1914
+ assert.equal(completeAdaptedRoleBinding(cwd, {
1915
+ sessionId: scope.session_id, parentThreadId: scope.parent_thread_id,
1916
+ correlationToken: ' validtoken ', claimantToken: ' claimant ', nowMs,
1917
+ }), 'claimant_mismatch');
1918
+ }
1919
+ finally {
1920
+ await rm(cwd, { recursive: true, force: true });
1921
+ }
1922
+ });
1923
+ it('rejects noncanonical caller correlation credentials without changing a retained intent', async () => {
1924
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-caller-credential-'));
1925
+ const correlationToken = canonicalCorrelationToken('caller-correlation');
1926
+ const scope = { sessionId: 'caller-session', parentThreadId: 'caller-parent', correlationToken, nowMs: Date.now() };
1927
+ try {
1928
+ assert.equal(recordPendingRoleIntent(cwd, { role: 'architect', ...scope }).ok, true);
1929
+ for (const invalid of ['', ' ', null, 1, true, [], {}]) {
1930
+ assert.equal(bindPendingRoleIntentUnderLock(cwd, { ...scope, correlationToken: invalid }, (state) => state), null);
1931
+ assert.equal(consumePendingRoleIntent(cwd, { ...scope, correlationToken: invalid }), null);
1932
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...scope, correlationToken: invalid }), 'not_found');
1933
+ }
1934
+ assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 1);
1935
+ }
1936
+ finally {
1937
+ await rm(cwd, { recursive: true, force: true });
1938
+ }
1939
+ });
1940
+ it('rejects every malformed completion credential without rewriting durable bound journals', async () => {
1941
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-completion-credentials-'));
1942
+ const nowMs = Date.now();
1943
+ const correlationToken = canonicalCorrelationToken('completion-correlation');
1944
+ const claimantToken = canonicalClaimantToken('completion-claimant');
1945
+ const scope = { sessionId: 'completion-session', parentThreadId: 'completion-parent', nowMs };
1946
+ const base = {
1947
+ role: 'architect',
1948
+ session_id: scope.sessionId,
1949
+ parent_thread_id: scope.parentThreadId,
1950
+ created_at: new Date(nowMs).toISOString(),
1951
+ expires_at: new Date(nowMs + 60_000).toISOString(),
1952
+ binding_state: 'bound',
1953
+ bound_at: new Date(nowMs).toISOString(),
1954
+ origin_cwd: cwd,
1955
+ };
1956
+ const writeJournal = async (journal) => {
1957
+ const raw = `${JSON.stringify({ schemaVersion: 1, sessions: {}, pending_role_intents: [journal] })}\n`;
1958
+ await writeFile(subagentTrackingPath(cwd), raw);
1959
+ return raw;
1960
+ };
1961
+ const assertRetainedMismatch = async (label, journal, input) => {
1962
+ const raw = await writeJournal(journal);
1963
+ assert.equal(completeAdaptedRoleBinding(cwd, input), 'claimant_mismatch', label);
1964
+ assert.equal(readFileSync(subagentTrackingPath(cwd), 'utf-8'), raw, label);
1965
+ };
1966
+ try {
1967
+ await mkdir(getBaseStateDir(cwd), { recursive: true });
1968
+ const claimantLess = { ...base, correlation_token: correlationToken };
1969
+ const claimed = { ...claimantLess, binding_claimant_token: claimantToken };
1970
+ const invalidCorrelations = [
1971
+ ['omitted', undefined], ['empty', ''], ['ascii-whitespace', ' '], ['tab-cr-lf-wrapped', `\t${correlationToken}\r\n`],
1972
+ ['uppercase', correlationToken.toUpperCase()], ['wrong-length', correlationToken.slice(1)], ['non-hex', 'g'.repeat(32)],
1973
+ ['wrong-canonical', canonicalCorrelationToken('other-correlation')], ['null', null], ['number', 1], ['boolean', true], ['array', []], ['object', {}],
1974
+ ];
1975
+ for (const [name, value] of invalidCorrelations) {
1976
+ const input = { ...scope, correlationToken: value, claimantToken };
1977
+ await assertRetainedMismatch(`claimant-less correlation ${name}`, claimantLess, input);
1978
+ await assertRetainedMismatch(`claimed correlation ${name}`, claimed, input);
1979
+ }
1980
+ const invalidClaimants = [
1981
+ ['omitted', undefined], ['empty', ''], ['ascii-whitespace', ' '], ['tab-cr-lf-wrapped', `\t${claimantToken}\r\n`],
1982
+ ['uppercase', claimantToken.toUpperCase()], ['wrong-length', claimantToken.slice(1)], ['non-hex', 'x'.repeat(36)],
1983
+ ['wrong-canonical', canonicalClaimantToken('other-claimant')], ['null', null], ['number', 1], ['boolean', true], ['array', []], ['object', {}],
1984
+ ];
1985
+ for (const [name, value] of invalidClaimants) {
1986
+ await assertRetainedMismatch(`claimed claimant ${name}`, claimed, { ...scope, correlationToken, claimantToken: value });
1987
+ }
1988
+ await writeJournal(claimantLess);
1989
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...scope, correlationToken }), 'completed');
1990
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, []);
1991
+ await writeJournal(claimed);
1992
+ assert.equal(completeAdaptedRoleBinding(cwd, { ...scope, correlationToken, claimantToken }), 'completed');
1993
+ assert.deepEqual((await readSubagentTrackingState(cwd)).pending_role_intents, []);
1994
+ for (const persistedCorrelation of ['', ' ', null, 1, true, [], {}]) {
1995
+ await assertRetainedMismatch('claimed persisted correlation', { ...claimed, correlation_token: persistedCorrelation }, { ...scope, correlationToken, claimantToken });
1996
+ await assertRetainedMismatch('claimant-less persisted correlation', { ...claimantLess, correlation_token: persistedCorrelation }, { ...scope, correlationToken });
1997
+ }
1998
+ for (const persistedClaimant of ['', ' ', null, 1, true, [], {}]) {
1999
+ await assertRetainedMismatch('claimed persisted claimant', { ...claimed, binding_claimant_token: persistedClaimant }, { ...scope, correlationToken, claimantToken });
2000
+ }
2001
+ }
2002
+ finally {
2003
+ await rm(cwd, { recursive: true, force: true });
2004
+ }
2005
+ });
2006
+ it('rejects NUL and ELOOP origins before creating tracking artifacts', async () => {
2007
+ const parent = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-origin-'));
2008
+ const stateRoot = await mkdtemp(join(tmpdir(), 'omx-subagent-tracker-origin-state-'));
2009
+ const loop = join(parent, 'loop');
2010
+ const validCwd = join(parent, 'valid');
2011
+ const previousStateRoot = process.env.OMX_STATE_ROOT;
2012
+ const assertNoArtifacts = () => assert.deepEqual(readdirSync(stateRoot), []);
2013
+ try {
2014
+ process.env.OMX_STATE_ROOT = stateRoot;
2015
+ await symlink(loop, loop);
2016
+ for (const cwd of [`${parent}\u0000suffix`, loop]) {
2017
+ assert.deepEqual(recordPendingRoleIntent(cwd, {
2018
+ role: 'architect', sessionId: 'origin-session', parentThreadId: 'origin-parent', correlationToken: canonicalCorrelationToken('origintoken'),
2019
+ }), { ok: false, reason: 'invalid_origin' });
2020
+ assertNoArtifacts();
2021
+ }
2022
+ await mkdir(validCwd);
2023
+ const recorded = recordPendingRoleIntent(validCwd, {
2024
+ role: 'architect', sessionId: 'valid-origin-session', parentThreadId: 'valid-origin-parent', correlationToken: canonicalCorrelationToken('validorigintoken'),
2025
+ });
2026
+ assert.equal(recorded.ok, true);
2027
+ assert.equal(recorded.ok && recorded.intent.origin_cwd, canonicalizeOriginCwd(validCwd));
2028
+ const state = await readSubagentTrackingState(validCwd);
2029
+ assert.deepEqual(state.pending_role_intents, recorded.ok ? [recorded.intent] : []);
2030
+ assert.equal(existsSync(subagentTrackingPath(validCwd)), true);
2031
+ }
2032
+ finally {
2033
+ if (previousStateRoot === undefined)
2034
+ delete process.env.OMX_STATE_ROOT;
2035
+ else
2036
+ process.env.OMX_STATE_ROOT = previousStateRoot;
2037
+ await rm(parent, { recursive: true, force: true });
2038
+ await rm(stateRoot, { recursive: true, force: true });
2039
+ }
2040
+ });
501
2041
  });
502
2042
  //# sourceMappingURL=tracker.test.js.map