oh-my-codex 0.20.1 → 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 (654) 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.map +1 -1
  36. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  37. package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
  38. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  40. package/dist/cli/__tests__/doctor-warning-copy.test.js +650 -15
  41. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  42. package/dist/cli/__tests__/exec.test.js.map +1 -1
  43. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  44. package/dist/cli/__tests__/index.test.js +216 -11
  45. package/dist/cli/__tests__/index.test.js.map +1 -1
  46. package/dist/cli/__tests__/launch-fallback.test.js +49 -3
  47. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  48. package/dist/cli/__tests__/mcp-parity.test.js +84 -12
  49. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  50. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  51. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  52. package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
  53. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  54. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  55. package/dist/cli/__tests__/question.test.js.map +1 -1
  56. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  57. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  58. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  59. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  60. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  61. package/dist/cli/__tests__/ralph.test.js +8 -3
  62. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  63. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  64. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  65. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
  66. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  67. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  68. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  69. package/dist/cli/__tests__/ralplan.test.js +129 -0
  70. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  71. package/dist/cli/__tests__/resume.test.js +6 -1
  72. package/dist/cli/__tests__/resume.test.js.map +1 -1
  73. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  74. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  75. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  76. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  77. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  78. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  79. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  80. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
  81. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  82. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  83. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  84. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  85. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  86. package/dist/cli/__tests__/setup-install-mode.test.js +2261 -14
  87. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  88. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  89. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  90. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  91. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  92. package/dist/cli/__tests__/setup-refresh.test.js +33 -2
  93. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  94. package/dist/cli/__tests__/setup-scope.test.js +46 -3
  95. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  96. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  97. package/dist/cli/__tests__/state.test.js +37 -0
  98. package/dist/cli/__tests__/state.test.js.map +1 -1
  99. package/dist/cli/__tests__/team.test.js.map +1 -1
  100. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  101. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  102. package/dist/cli/__tests__/uninstall.test.js +1771 -24
  103. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  104. package/dist/cli/__tests__/update.test.js +58 -0
  105. package/dist/cli/__tests__/update.test.js.map +1 -1
  106. package/dist/cli/__tests__/url.test.js.map +1 -1
  107. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  108. package/dist/cli/adapt.js.map +1 -1
  109. package/dist/cli/agents-init.js.map +1 -1
  110. package/dist/cli/agents.js.map +1 -1
  111. package/dist/cli/api.js.map +1 -1
  112. package/dist/cli/ask.d.ts +1 -1
  113. package/dist/cli/ask.d.ts.map +1 -1
  114. package/dist/cli/ask.js.map +1 -1
  115. package/dist/cli/auth.js.map +1 -1
  116. package/dist/cli/autoresearch-guided.js.map +1 -1
  117. package/dist/cli/autoresearch-intake.js.map +1 -1
  118. package/dist/cli/capabilities.js.map +1 -1
  119. package/dist/cli/cleanup.js.map +1 -1
  120. package/dist/cli/codex-feature-probe.js.map +1 -1
  121. package/dist/cli/codex-home.js.map +1 -1
  122. package/dist/cli/doctor.d.ts +18 -0
  123. package/dist/cli/doctor.d.ts.map +1 -1
  124. package/dist/cli/doctor.js +594 -130
  125. package/dist/cli/doctor.js.map +1 -1
  126. package/dist/cli/explore.js.map +1 -1
  127. package/dist/cli/index.d.ts +11 -3
  128. package/dist/cli/index.d.ts.map +1 -1
  129. package/dist/cli/index.js +108 -20
  130. package/dist/cli/index.js.map +1 -1
  131. package/dist/cli/mcp-serve.js.map +1 -1
  132. package/dist/cli/mission.js.map +1 -1
  133. package/dist/cli/native-assets.js.map +1 -1
  134. package/dist/cli/native-hook-claim-journal.d.ts +18 -0
  135. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  136. package/dist/cli/native-hook-claim-journal.js +239 -0
  137. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  138. package/dist/cli/omx.js.map +1 -1
  139. package/dist/cli/plugin-marketplace.js.map +1 -1
  140. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  141. package/dist/cli/question.js.map +1 -1
  142. package/dist/cli/ralph.d.ts.map +1 -1
  143. package/dist/cli/ralph.js +3 -2
  144. package/dist/cli/ralph.js.map +1 -1
  145. package/dist/cli/ralplan.d.ts +14 -0
  146. package/dist/cli/ralplan.d.ts.map +1 -0
  147. package/dist/cli/ralplan.js +203 -0
  148. package/dist/cli/ralplan.js.map +1 -0
  149. package/dist/cli/setup-preferences.d.ts +11 -0
  150. package/dist/cli/setup-preferences.d.ts.map +1 -1
  151. package/dist/cli/setup-preferences.js +32 -0
  152. package/dist/cli/setup-preferences.js.map +1 -1
  153. package/dist/cli/setup.d.ts +53 -0
  154. package/dist/cli/setup.d.ts.map +1 -1
  155. package/dist/cli/setup.js +1401 -398
  156. package/dist/cli/setup.js.map +1 -1
  157. package/dist/cli/sparkshell.js.map +1 -1
  158. package/dist/cli/star-prompt.js.map +1 -1
  159. package/dist/cli/state.d.ts.map +1 -1
  160. package/dist/cli/state.js +2 -1
  161. package/dist/cli/state.js.map +1 -1
  162. package/dist/cli/team.js.map +1 -1
  163. package/dist/cli/ultragoal.d.ts +5 -1
  164. package/dist/cli/ultragoal.d.ts.map +1 -1
  165. package/dist/cli/ultragoal.js +34 -2
  166. package/dist/cli/ultragoal.js.map +1 -1
  167. package/dist/cli/uninstall.d.ts +8 -0
  168. package/dist/cli/uninstall.d.ts.map +1 -1
  169. package/dist/cli/uninstall.js +944 -95
  170. package/dist/cli/uninstall.js.map +1 -1
  171. package/dist/cli/update.d.ts.map +1 -1
  172. package/dist/cli/update.js +8 -1
  173. package/dist/cli/update.js.map +1 -1
  174. package/dist/cli/url.js.map +1 -1
  175. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  176. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  177. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  178. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  179. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  180. package/dist/config/__tests__/generator-idempotent.test.js +850 -32
  181. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  182. package/dist/config/__tests__/generator-notify.test.js +6 -1
  183. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  184. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  185. package/dist/config/codex-feature-flags.js.map +1 -1
  186. package/dist/config/codex-hooks.d.ts +124 -1
  187. package/dist/config/codex-hooks.d.ts.map +1 -1
  188. package/dist/config/codex-hooks.js +2142 -265
  189. package/dist/config/codex-hooks.js.map +1 -1
  190. package/dist/config/commit-lore-guard.js.map +1 -1
  191. package/dist/config/generator.d.ts +21 -16
  192. package/dist/config/generator.d.ts.map +1 -1
  193. package/dist/config/generator.js +991 -137
  194. package/dist/config/generator.js.map +1 -1
  195. package/dist/config/mcp-registry.js.map +1 -1
  196. package/dist/config/models.d.ts +2 -2
  197. package/dist/config/models.d.ts.map +1 -1
  198. package/dist/config/models.js.map +1 -1
  199. package/dist/config/omx-first-party-mcp.js.map +1 -1
  200. package/dist/config/team-mode.d.ts.map +1 -1
  201. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  202. package/dist/document-refresh/enforcer.js.map +1 -1
  203. package/dist/exec/followup.js.map +1 -1
  204. package/dist/goal-workflows/artifacts.js.map +1 -1
  205. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  206. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  207. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  208. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  209. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  210. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  211. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  212. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  213. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  214. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  215. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  216. package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
  217. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  218. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
  219. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  220. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  221. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
  222. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  223. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  224. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  225. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  226. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  227. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  228. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  229. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  230. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  231. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  232. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  233. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  234. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  235. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  236. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  237. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  238. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
  239. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  240. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  241. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  242. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  243. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  244. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  245. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  246. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  247. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  248. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  249. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  250. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  251. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  252. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  253. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  254. package/dist/hooks/__tests__/session.test.js +532 -11
  255. package/dist/hooks/__tests__/session.test.js.map +1 -1
  256. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  257. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  258. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  259. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  260. package/dist/hooks/agents-overlay.js +5 -5
  261. package/dist/hooks/agents-overlay.js.map +1 -1
  262. package/dist/hooks/code-simplifier/index.js.map +1 -1
  263. package/dist/hooks/explore-routing.js.map +1 -1
  264. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  265. package/dist/hooks/extensibility/events.js.map +1 -1
  266. package/dist/hooks/extensibility/loader.js.map +1 -1
  267. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  268. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  269. package/dist/hooks/keyword-detector.d.ts +33 -7
  270. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  271. package/dist/hooks/keyword-detector.js +2228 -119
  272. package/dist/hooks/keyword-detector.js.map +1 -1
  273. package/dist/hooks/keyword-registry.d.ts +12 -0
  274. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  275. package/dist/hooks/keyword-registry.js +41 -0
  276. package/dist/hooks/keyword-registry.js.map +1 -1
  277. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  278. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  279. package/dist/hooks/prompt-session-provenance.js +180 -0
  280. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  281. package/dist/hooks/session.d.ts +139 -34
  282. package/dist/hooks/session.d.ts.map +1 -1
  283. package/dist/hooks/session.js +1239 -317
  284. package/dist/hooks/session.js.map +1 -1
  285. package/dist/hooks/task-size-detector.js.map +1 -1
  286. package/dist/hud/__tests__/state.test.js.map +1 -1
  287. package/dist/hud/authority.js.map +1 -1
  288. package/dist/hud/constants.js.map +1 -1
  289. package/dist/hud/index.js.map +1 -1
  290. package/dist/hud/reconcile.js.map +1 -1
  291. package/dist/hud/render.js.map +1 -1
  292. package/dist/hud/state.js.map +1 -1
  293. package/dist/hud/tmux.js.map +1 -1
  294. package/dist/leader/__tests__/contract.test.js +103 -1
  295. package/dist/leader/__tests__/contract.test.js.map +1 -1
  296. package/dist/leader/contract.d.ts +26 -5
  297. package/dist/leader/contract.d.ts.map +1 -1
  298. package/dist/leader/contract.js +109 -3
  299. package/dist/leader/contract.js.map +1 -1
  300. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  301. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  302. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  303. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  304. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  305. package/dist/mcp/bootstrap.js.map +1 -1
  306. package/dist/mcp/code-intel-server.d.ts +58 -58
  307. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  308. package/dist/mcp/code-intel-server.js.map +1 -1
  309. package/dist/mcp/hermes-bridge.js.map +1 -1
  310. package/dist/mcp/hermes-server.d.ts +70 -70
  311. package/dist/mcp/hermes-server.d.ts.map +1 -1
  312. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  313. package/dist/mcp/memory-server.d.ts +34 -34
  314. package/dist/mcp/memory-server.d.ts.map +1 -1
  315. package/dist/mcp/memory-server.js.map +1 -1
  316. package/dist/mcp/state-paths.d.ts +14 -0
  317. package/dist/mcp/state-paths.d.ts.map +1 -1
  318. package/dist/mcp/state-paths.js +96 -20
  319. package/dist/mcp/state-paths.js.map +1 -1
  320. package/dist/mcp/state-server.d.ts +47 -47
  321. package/dist/mcp/state-server.d.ts.map +1 -1
  322. package/dist/mcp/trace-server.d.ts +4 -4
  323. package/dist/mcp/trace-server.d.ts.map +1 -1
  324. package/dist/mcp/trace-server.js.map +1 -1
  325. package/dist/mcp/wiki-server.d.ts +23 -23
  326. package/dist/mcp/wiki-server.d.ts.map +1 -1
  327. package/dist/mcp/wiki-server.js.map +1 -1
  328. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  329. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  330. package/dist/modes/base.d.ts.map +1 -1
  331. package/dist/modes/base.js +5 -5
  332. package/dist/modes/base.js.map +1 -1
  333. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  334. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  335. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  336. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  337. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  338. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  339. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  340. package/dist/notifications/http-client.js.map +1 -1
  341. package/dist/notifications/index.d.ts +4 -1
  342. package/dist/notifications/index.d.ts.map +1 -1
  343. package/dist/notifications/index.js +3 -9
  344. package/dist/notifications/index.js.map +1 -1
  345. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  346. package/dist/notifications/reply-listener.d.ts.map +1 -1
  347. package/dist/notifications/reply-listener.js.map +1 -1
  348. package/dist/notifications/session-registry.js.map +1 -1
  349. package/dist/notifications/session-status.js.map +1 -1
  350. package/dist/notifications/temp-contract.js.map +1 -1
  351. package/dist/notifications/tmux-detector.js.map +1 -1
  352. package/dist/notifications/tmux.js.map +1 -1
  353. package/dist/openclaw/config.js.map +1 -1
  354. package/dist/openclaw/dispatcher.js.map +1 -1
  355. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  356. package/dist/pipeline/__tests__/stages.test.js +8 -5
  357. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  358. package/dist/pipeline/stages/code-review.js.map +1 -1
  359. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  360. package/dist/pipeline/stages/ralplan.js.map +1 -1
  361. package/dist/pipeline/stages/team-exec.js.map +1 -1
  362. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  363. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  364. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  365. package/dist/planning/artifact-names.js.map +1 -1
  366. package/dist/planning/artifacts.js.map +1 -1
  367. package/dist/question/__tests__/state.test.js.map +1 -1
  368. package/dist/question/__tests__/ui.test.js.map +1 -1
  369. package/dist/question/autopilot-wait.js.map +1 -1
  370. package/dist/question/client.d.ts.map +1 -1
  371. package/dist/question/client.js.map +1 -1
  372. package/dist/question/deep-interview.js.map +1 -1
  373. package/dist/question/events.js.map +1 -1
  374. package/dist/question/renderer.js.map +1 -1
  375. package/dist/question/state.d.ts.map +1 -1
  376. package/dist/question/state.js.map +1 -1
  377. package/dist/question/ui.js.map +1 -1
  378. package/dist/ralph/contract.d.ts +1 -1
  379. package/dist/ralph/contract.d.ts.map +1 -1
  380. package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
  381. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  382. package/dist/ralplan/__tests__/runtime.test.js +112 -14
  383. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  384. package/dist/ralplan/consensus-gate.d.ts +3 -3
  385. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  386. package/dist/ralplan/consensus-gate.js +155 -60
  387. package/dist/ralplan/consensus-gate.js.map +1 -1
  388. package/dist/ralplan/runtime.d.ts +2 -2
  389. package/dist/ralplan/runtime.d.ts.map +1 -1
  390. package/dist/ralplan/runtime.js +1 -2
  391. package/dist/ralplan/runtime.js.map +1 -1
  392. package/dist/runtime/bridge.d.ts.map +1 -1
  393. package/dist/runtime/bridge.js.map +1 -1
  394. package/dist/runtime/process-tree.js.map +1 -1
  395. package/dist/runtime/run-loop.js.map +1 -1
  396. package/dist/runtime/run-outcome.d.ts +3 -3
  397. package/dist/runtime/run-outcome.d.ts.map +1 -1
  398. package/dist/runtime/run-outcome.js.map +1 -1
  399. package/dist/runtime/run-state.js.map +1 -1
  400. package/dist/scripts/__tests__/codex-native-hook.test.js +4121 -592
  401. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  402. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  403. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  404. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  405. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  406. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  407. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  408. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  409. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  410. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  411. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  412. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  413. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  414. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  415. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  416. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  417. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
  418. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  419. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  420. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  421. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
  422. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  423. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  424. package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
  425. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  426. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  427. package/dist/scripts/build-api.js.map +1 -1
  428. package/dist/scripts/build-explore-harness.js.map +1 -1
  429. package/dist/scripts/build-sparkshell.js.map +1 -1
  430. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  431. package/dist/scripts/codex-execution-surface.js.map +1 -1
  432. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  433. package/dist/scripts/codex-native-hook.js +689 -156
  434. package/dist/scripts/codex-native-hook.js.map +1 -1
  435. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  436. package/dist/scripts/generate-release-body.js.map +1 -1
  437. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  438. package/dist/scripts/hook-payload-guard.js.map +1 -1
  439. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  440. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  441. package/dist/scripts/notify-fallback-delivery.js +419 -0
  442. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  443. package/dist/scripts/notify-fallback-watcher.js +136 -30
  444. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  445. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  446. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  447. package/dist/scripts/notify-hook/auto-nudge.js +39 -13
  448. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  449. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  450. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  451. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  452. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  453. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  454. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  455. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  456. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  457. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  458. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  459. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  460. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  461. package/dist/scripts/notify-hook/state-io.js +73 -11
  462. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  463. package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
  464. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  466. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
  467. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  468. package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
  469. package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
  470. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  471. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  472. package/dist/scripts/notify-hook/tmux-injection.js +25 -16
  473. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  474. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  475. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  476. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  477. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  478. package/dist/scripts/notify-hook.d.ts +24 -1
  479. package/dist/scripts/notify-hook.d.ts.map +1 -1
  480. package/dist/scripts/notify-hook.js +266 -90
  481. package/dist/scripts/notify-hook.js.map +1 -1
  482. package/dist/scripts/postinstall.js.map +1 -1
  483. package/dist/scripts/prompt-inventory.js.map +1 -1
  484. package/dist/scripts/smoke-packed-install.d.ts +879 -2
  485. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  486. package/dist/scripts/smoke-packed-install.js +1528 -7
  487. package/dist/scripts/smoke-packed-install.js.map +1 -1
  488. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  489. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  490. package/dist/scripts/test-sparkshell.js.map +1 -1
  491. package/dist/scripts/verify-native-agents.js.map +1 -1
  492. package/dist/session-history/friction.js.map +1 -1
  493. package/dist/session-history/search.js.map +1 -1
  494. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  495. package/dist/sidecar/collector.js.map +1 -1
  496. package/dist/sidecar/index.js.map +1 -1
  497. package/dist/sidecar/render.js.map +1 -1
  498. package/dist/sidecar/tmux.js.map +1 -1
  499. package/dist/state/__tests__/operations.test.js +64 -15
  500. package/dist/state/__tests__/operations.test.js.map +1 -1
  501. package/dist/state/__tests__/skill-active.test.js +19 -1
  502. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  503. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  504. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  505. package/dist/state/mode-state-context.js.map +1 -1
  506. package/dist/state/operations.d.ts +1 -1
  507. package/dist/state/operations.d.ts.map +1 -1
  508. package/dist/state/operations.js +49 -55
  509. package/dist/state/operations.js.map +1 -1
  510. package/dist/state/paths.d.ts +1 -1
  511. package/dist/state/paths.d.ts.map +1 -1
  512. package/dist/state/paths.js +1 -1
  513. package/dist/state/paths.js.map +1 -1
  514. package/dist/state/skill-active.d.ts +13 -1
  515. package/dist/state/skill-active.d.ts.map +1 -1
  516. package/dist/state/skill-active.js +43 -4
  517. package/dist/state/skill-active.js.map +1 -1
  518. package/dist/state/terminal-normalization.js.map +1 -1
  519. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  520. package/dist/state/workflow-transition-reconcile.js +20 -4
  521. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  522. package/dist/state/workflow-transition.d.ts +1 -1
  523. package/dist/state/workflow-transition.d.ts.map +1 -1
  524. package/dist/state/workflow-transition.js.map +1 -1
  525. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
  526. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
  527. package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
  528. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
  529. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  530. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  531. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
  532. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  533. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  534. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  535. package/dist/subagents/adapted-role-binding.d.ts +17 -0
  536. package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
  537. package/dist/subagents/adapted-role-binding.js +75 -0
  538. package/dist/subagents/adapted-role-binding.js.map +1 -0
  539. package/dist/subagents/role-routing-marker.d.ts +10 -0
  540. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  541. package/dist/subagents/role-routing-marker.js +136 -0
  542. package/dist/subagents/role-routing-marker.js.map +1 -0
  543. package/dist/subagents/tracker.d.ts +172 -0
  544. package/dist/subagents/tracker.d.ts.map +1 -1
  545. package/dist/subagents/tracker.js +1077 -61
  546. package/dist/subagents/tracker.js.map +1 -1
  547. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  548. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  549. package/dist/team/__tests__/model-contract.test.js +231 -1
  550. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  551. package/dist/team/__tests__/role-router.test.js.map +1 -1
  552. package/dist/team/__tests__/runtime.test.js +401 -1
  553. package/dist/team/__tests__/runtime.test.js.map +1 -1
  554. package/dist/team/__tests__/scaling.test.js +181 -0
  555. package/dist/team/__tests__/scaling.test.js.map +1 -1
  556. package/dist/team/__tests__/state-root.test.js.map +1 -1
  557. package/dist/team/__tests__/tmux-session.test.js +434 -4
  558. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  559. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  560. package/dist/team/api-interop.d.ts +2 -2
  561. package/dist/team/api-interop.d.ts.map +1 -1
  562. package/dist/team/api-interop.js.map +1 -1
  563. package/dist/team/commit-hygiene.js.map +1 -1
  564. package/dist/team/contracts.d.ts +5 -5
  565. package/dist/team/contracts.d.ts.map +1 -1
  566. package/dist/team/delivery-log.js.map +1 -1
  567. package/dist/team/followup-planner.js.map +1 -1
  568. package/dist/team/goal-workflow.js.map +1 -1
  569. package/dist/team/idle-nudge.d.ts.map +1 -1
  570. package/dist/team/model-contract.d.ts +20 -1
  571. package/dist/team/model-contract.d.ts.map +1 -1
  572. package/dist/team/model-contract.js +267 -30
  573. package/dist/team/model-contract.js.map +1 -1
  574. package/dist/team/orchestrator.js.map +1 -1
  575. package/dist/team/pane-status.js.map +1 -1
  576. package/dist/team/phase-controller.js.map +1 -1
  577. package/dist/team/reminder-intents.d.ts +1 -1
  578. package/dist/team/reminder-intents.d.ts.map +1 -1
  579. package/dist/team/reminder-intents.js.map +1 -1
  580. package/dist/team/runtime-cli.js.map +1 -1
  581. package/dist/team/runtime.d.ts.map +1 -1
  582. package/dist/team/runtime.js +83 -52
  583. package/dist/team/runtime.js.map +1 -1
  584. package/dist/team/scaling.d.ts +9 -7
  585. package/dist/team/scaling.d.ts.map +1 -1
  586. package/dist/team/scaling.js +69 -28
  587. package/dist/team/scaling.js.map +1 -1
  588. package/dist/team/state/dispatch-lock.js.map +1 -1
  589. package/dist/team/state/dispatch.js.map +1 -1
  590. package/dist/team/state/events.js.map +1 -1
  591. package/dist/team/state/tasks.js.map +1 -1
  592. package/dist/team/state-root.js.map +1 -1
  593. package/dist/team/state.js.map +1 -1
  594. package/dist/team/team-identity.d.ts.map +1 -1
  595. package/dist/team/team-identity.js.map +1 -1
  596. package/dist/team/tmux-session.d.ts +2 -0
  597. package/dist/team/tmux-session.d.ts.map +1 -1
  598. package/dist/team/tmux-session.js +90 -42
  599. package/dist/team/tmux-session.js.map +1 -1
  600. package/dist/team/worker-bootstrap.js.map +1 -1
  601. package/dist/team/worktree.js.map +1 -1
  602. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  603. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  604. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  605. package/dist/ultragoal/artifacts.js +4 -1
  606. package/dist/ultragoal/artifacts.js.map +1 -1
  607. package/dist/url-reader/index.js.map +1 -1
  608. package/dist/utils/agents-model-table.js.map +1 -1
  609. package/dist/utils/package.js.map +1 -1
  610. package/dist/utils/paths.js.map +1 -1
  611. package/dist/utils/platform-command.js.map +1 -1
  612. package/dist/utils/repo-deps.js.map +1 -1
  613. package/dist/utils/version.js.map +1 -1
  614. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  615. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  616. package/dist/verification/verifier.js.map +1 -1
  617. package/dist/visual/constants.d.ts +1 -1
  618. package/dist/visual/constants.d.ts.map +1 -1
  619. package/dist/vscode/__tests__/index.test.js.map +1 -1
  620. package/dist/vscode/index.js.map +1 -1
  621. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  622. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  623. package/dist/wiki/__tests__/query.test.js.map +1 -1
  624. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  625. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  626. package/dist/wiki/lint.js.map +1 -1
  627. package/dist/wiki/query.js.map +1 -1
  628. package/dist/wiki/storage.js.map +1 -1
  629. package/package.json +2 -2
  630. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  631. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  632. package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
  633. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
  634. package/skills/omx-setup/SKILL.md +10 -5
  635. package/skills/ralph/SKILL.md +4 -2
  636. package/skills/ralplan/SKILL.md +5 -2
  637. package/src/scripts/__tests__/codex-native-hook.test.ts +17522 -10630
  638. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  639. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  640. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  641. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
  642. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
  643. package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
  644. package/src/scripts/codex-native-hook.ts +779 -159
  645. package/src/scripts/notify-fallback-delivery.ts +416 -0
  646. package/src/scripts/notify-fallback-watcher.ts +132 -40
  647. package/src/scripts/notify-hook/auto-nudge.ts +43 -12
  648. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  649. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  650. package/src/scripts/notify-hook/state-io.ts +100 -13
  651. package/src/scripts/notify-hook/tmux-injection.ts +38 -21
  652. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  653. package/src/scripts/notify-hook.ts +335 -90
  654. package/src/scripts/smoke-packed-install.ts +1829 -6
@@ -1,16 +1,47 @@
1
- import { existsSync } from 'node:fs';
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { dirname, join } from 'node:path';
4
- import { getBaseStateDir } from '../state/paths.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, existsSync, ftruncateSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
3
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { hostname } from 'node:os';
5
+ import { basename, dirname, join } from 'node:path';
6
+ import { AGENT_DEFINITIONS } from '../agents/definitions.js';
7
+ import { getBaseStateDir, getBaseStateDirWithSource } from '../state/paths.js';
8
+ import { canonicalizeOriginCwd } from '../leader/contract.js';
9
+ import { codexAgentsDir, projectCodexAgentsDir } from '../utils/paths.js';
5
10
  export const SUBAGENT_TRACKING_SCHEMA_VERSION = 1;
6
11
  export const DEFAULT_SUBAGENT_ACTIVE_WINDOW_MS = 120_000;
12
+ export const OMX_ADAPTED_PROVENANCE = 'omx_adapted';
13
+ export const NATIVE_SUBAGENT_PROVENANCE = 'native_subagent';
14
+ const KNOWN_TYPED_AGENT_ROLES = new Set(Object.keys(AGENT_DEFINITIONS).map((role) => role.toLowerCase()));
7
15
  export function subagentTrackingPath(cwd) {
8
16
  return join(getBaseStateDir(cwd), 'subagent-tracking.json');
9
17
  }
18
+ export function resolveInstalledRoleName(role, codexHomeOverride) {
19
+ const normalizedRole = role.trim().toLowerCase();
20
+ if (!normalizedRole)
21
+ return null;
22
+ if (KNOWN_TYPED_AGENT_ROLES.has(normalizedRole))
23
+ return normalizedRole;
24
+ for (const agentsDir of [codexAgentsDir(codexHomeOverride), projectCodexAgentsDir()]) {
25
+ try {
26
+ for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
27
+ if (!entry.isFile() || !entry.name.endsWith('.toml'))
28
+ continue;
29
+ const installedRole = entry.name.slice(0, -'.toml'.length).trim().toLowerCase();
30
+ if (installedRole === normalizedRole)
31
+ return installedRole;
32
+ }
33
+ }
34
+ catch {
35
+ // Missing or unreadable agent directories do not invalidate built-in roles.
36
+ }
37
+ }
38
+ return null;
39
+ }
10
40
  export function createSubagentTrackingState() {
11
41
  return {
12
42
  schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
13
43
  sessions: {},
44
+ pending_role_intents: [],
14
45
  };
15
46
  }
16
47
  function normalizeSubagentStatus(value) {
@@ -88,6 +119,40 @@ export function isTrustedSubagentThread(session, threadId) {
88
119
  return false;
89
120
  return session.threads[normalizedThreadId]?.kind === 'subagent';
90
121
  }
122
+ function normalizePendingRoleIntent(value) {
123
+ if (!value || typeof value !== 'object')
124
+ return null;
125
+ const candidate = value;
126
+ const role = readOptionalTrimmedString(candidate.role);
127
+ const sessionId = readOptionalTrimmedString(candidate.session_id);
128
+ const parentThreadId = readOptionalTrimmedString(candidate.parent_thread_id);
129
+ const createdAt = readOptionalTrimmedString(candidate.created_at);
130
+ const expiresAt = readOptionalTrimmedString(candidate.expires_at);
131
+ if (!role || !sessionId || !parentThreadId || !Object.hasOwn(candidate, 'correlation_token') || !createdAt || !expiresAt)
132
+ return null;
133
+ if (!Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(expiresAt)))
134
+ return null;
135
+ const bindingState = candidate.binding_state === 'bound' ? 'bound' : undefined;
136
+ const boundAt = readOptionalTrimmedString(candidate.bound_at);
137
+ const hasValidBoundAt = Boolean(boundAt && Number.isFinite(Date.parse(boundAt)));
138
+ const originCwd = readOptionalTrimmedString(candidate.origin_cwd);
139
+ return {
140
+ role,
141
+ session_id: sessionId,
142
+ parent_thread_id: parentThreadId,
143
+ // Bound journals are durable security records. Retain malformed credentials verbatim so
144
+ // completion can reject them rather than silently converting them into claimant-less data.
145
+ correlation_token: candidate.correlation_token,
146
+ created_at: createdAt,
147
+ expires_at: expiresAt,
148
+ ...(bindingState ? { binding_state: bindingState } : {}),
149
+ ...(bindingState && Object.hasOwn(candidate, 'binding_claimant_token')
150
+ ? { binding_claimant_token: candidate.binding_claimant_token }
151
+ : {}),
152
+ ...(bindingState && hasValidBoundAt ? { bound_at: boundAt } : {}),
153
+ ...(originCwd ? { origin_cwd: originCwd } : {}),
154
+ };
155
+ }
91
156
  export function normalizeSubagentTrackingState(input) {
92
157
  const base = createSubagentTrackingState();
93
158
  if (!input || typeof input !== 'object')
@@ -102,9 +167,7 @@ export function normalizeSubagentTrackingState(input) {
102
167
  if (!rawThread || typeof rawThread !== 'object')
103
168
  continue;
104
169
  const candidate = rawThread;
105
- const normalizedThreadId = typeof candidate.thread_id === 'string' && candidate.thread_id.trim().length > 0
106
- ? candidate.thread_id.trim()
107
- : threadId.trim();
170
+ const normalizedThreadId = typeof candidate.thread_id === 'string' && candidate.thread_id.trim().length > 0 ? candidate.thread_id.trim() : threadId.trim();
108
171
  if (!normalizedThreadId)
109
172
  continue;
110
173
  const kind = candidate.kind === 'leader' ? 'leader' : 'subagent';
@@ -113,32 +176,31 @@ export function normalizeSubagentTrackingState(input) {
113
176
  : typeof candidate.last_seen_at === 'string' && candidate.last_seen_at.trim().length > 0
114
177
  ? candidate.last_seen_at
115
178
  : new Date(0).toISOString();
116
- const lastSeenAt = typeof candidate.last_seen_at === 'string' && candidate.last_seen_at.trim().length > 0
117
- ? candidate.last_seen_at
118
- : firstSeenAt;
179
+ const lastSeenAt = typeof candidate.last_seen_at === 'string' && candidate.last_seen_at.trim().length > 0 ? candidate.last_seen_at : firstSeenAt;
119
180
  threads[normalizedThreadId] = {
120
181
  thread_id: normalizedThreadId,
121
182
  kind,
122
183
  first_seen_at: firstSeenAt,
123
184
  last_seen_at: lastSeenAt,
124
- ...(typeof candidate.last_turn_id === 'string' && candidate.last_turn_id.trim().length > 0
125
- ? { last_turn_id: candidate.last_turn_id }
126
- : {}),
127
- ...(typeof candidate.completed_at === 'string' && candidate.completed_at.trim().length > 0
128
- ? { completed_at: candidate.completed_at }
129
- : {}),
185
+ ...(typeof candidate.last_turn_id === 'string' && candidate.last_turn_id.trim().length > 0 ? { last_turn_id: candidate.last_turn_id } : {}),
186
+ ...(typeof candidate.completed_at === 'string' && candidate.completed_at.trim().length > 0 ? { completed_at: candidate.completed_at } : {}),
130
187
  ...(typeof candidate.last_completed_turn_id === 'string' && candidate.last_completed_turn_id.trim().length > 0
131
188
  ? { last_completed_turn_id: candidate.last_completed_turn_id }
132
189
  : {}),
133
- turn_count: typeof candidate.turn_count === 'number' && Number.isFinite(candidate.turn_count) && candidate.turn_count > 0
134
- ? candidate.turn_count
135
- : 1,
190
+ turn_count: typeof candidate.turn_count === 'number' && Number.isFinite(candidate.turn_count) && candidate.turn_count > 0 ? candidate.turn_count : 1,
136
191
  ...(typeof candidate.mode === 'string' && candidate.mode.trim().length > 0 ? { mode: candidate.mode } : {}),
137
192
  ...(typeof candidate.role === 'string' && candidate.role.trim().length > 0 ? { role: candidate.role.trim() } : {}),
193
+ ...(typeof candidate.provenance_kind === 'string' && candidate.provenance_kind.trim().length > 0
194
+ ? { provenance_kind: candidate.provenance_kind.trim() }
195
+ : {}),
138
196
  ...(typeof candidate.lane_id === 'string' && candidate.lane_id.trim().length > 0 ? { lane_id: candidate.lane_id.trim() } : {}),
139
197
  ...(typeof candidate.scope === 'string' && candidate.scope.trim().length > 0 ? { scope: candidate.scope.trim() } : {}),
140
- ...(typeof candidate.agent_nickname === 'string' && candidate.agent_nickname.trim().length > 0 ? { agent_nickname: candidate.agent_nickname.trim() } : {}),
141
- ...(typeof candidate.completion_source === 'string' && candidate.completion_source.trim().length > 0 ? { completion_source: candidate.completion_source } : {}),
198
+ ...(typeof candidate.agent_nickname === 'string' && candidate.agent_nickname.trim().length > 0
199
+ ? { agent_nickname: candidate.agent_nickname.trim() }
200
+ : {}),
201
+ ...(typeof candidate.completion_source === 'string' && candidate.completion_source.trim().length > 0
202
+ ? { completion_source: candidate.completion_source }
203
+ : {}),
142
204
  ...(normalizeSubagentStatus(candidate.status) ? { status: normalizeSubagentStatus(candidate.status) } : {}),
143
205
  ...(typeof candidate.last_handoff_summary === 'string' && candidate.last_handoff_summary.trim().length > 0
144
206
  ? { last_handoff_summary: candidate.last_handoff_summary.trim() }
@@ -158,8 +220,12 @@ export function normalizeSubagentTrackingState(input) {
158
220
  };
159
221
  }
160
222
  const sessionCandidate = rawSession;
161
- const leaderThreadId = typeof sessionCandidate.leader_thread_id === 'string'
162
- ? sessionCandidate.leader_thread_id.trim() || undefined
223
+ const leaderThreadId = typeof sessionCandidate.leader_thread_id === 'string' ? sessionCandidate.leader_thread_id.trim() || undefined : undefined;
224
+ const leaderAttestedAt = typeof sessionCandidate.leader_attested_at === 'string' && sessionCandidate.leader_attested_at.trim().length > 0
225
+ ? sessionCandidate.leader_attested_at.trim()
226
+ : undefined;
227
+ const leaderAttestSource = typeof sessionCandidate.leader_attest_source === 'string' && sessionCandidate.leader_attest_source.trim().length > 0
228
+ ? sessionCandidate.leader_attest_source.trim()
163
229
  : undefined;
164
230
  const updatedAt = typeof sessionCandidate.updated_at === 'string' && sessionCandidate.updated_at.trim().length > 0
165
231
  ? sessionCandidate.updated_at
@@ -167,15 +233,437 @@ export function normalizeSubagentTrackingState(input) {
167
233
  sessions[sessionId] = {
168
234
  session_id: sessionId,
169
235
  leader_thread_id: leaderThreadId,
236
+ ...(leaderAttestedAt ? { leader_attested_at: leaderAttestedAt } : {}),
237
+ ...(leaderAttestSource ? { leader_attest_source: leaderAttestSource } : {}),
170
238
  updated_at: updatedAt,
171
239
  threads,
172
240
  };
173
241
  }
242
+ const pendingRoleIntents = Array.isArray(parsed.pending_role_intents)
243
+ ? parsed.pending_role_intents.map((intent) => normalizePendingRoleIntent(intent)).filter((intent) => intent !== null)
244
+ : [];
174
245
  return {
175
246
  schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
176
247
  sessions,
248
+ pending_role_intents: pendingRoleIntents,
177
249
  };
178
250
  }
251
+ function atomicTrackingTempPath(path) {
252
+ return `${path}.${process.pid}.${randomUUID()}.tmp`;
253
+ }
254
+ export const DEFAULT_CROSS_PROCESS_LOCK_MAX_ATTEMPTS = 80;
255
+ export const DEFAULT_CROSS_PROCESS_LOCK_RETRY_MS = 2;
256
+ export const CROSS_PROCESS_LOCK_LEASE_MS = 60_000;
257
+ const crossProcessLockWaitArray = new Int32Array(new SharedArrayBuffer(4));
258
+ export class CrossProcessLockLostError extends Error {
259
+ constructor(lockPath) {
260
+ super(`Lost cross-process lock ownership at ${lockPath}`);
261
+ this.name = 'CrossProcessLockLostError';
262
+ }
263
+ }
264
+ export function crossProcessLockPath(resourcePath) {
265
+ return `${resourcePath}.lock`;
266
+ }
267
+ function sleepForCrossProcessLockSync(durationMs) {
268
+ Atomics.wait(crossProcessLockWaitArray, 0, 0, durationMs);
269
+ }
270
+ export function readProcessStartIdentity(pid) {
271
+ if (process.platform !== 'linux')
272
+ return undefined;
273
+ try {
274
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
275
+ const closingParenthesis = stat.lastIndexOf(')');
276
+ const fields = stat.slice(closingParenthesis + 1).trim().split(/\s+/);
277
+ const starttime = fields[19];
278
+ const bootId = readFileSync('/proc/sys/kernel/random/boot_id', 'utf-8').trim();
279
+ if (closingParenthesis < 0 || !starttime || !/^\d+$/.test(starttime) || !bootId)
280
+ return undefined;
281
+ return `${bootId}:${starttime}`;
282
+ }
283
+ catch {
284
+ return undefined;
285
+ }
286
+ }
287
+ function createCrossProcessLockClaim(token) {
288
+ const pidStartId = readProcessStartIdentity(process.pid);
289
+ return {
290
+ token,
291
+ pid: process.pid,
292
+ host: hostname(),
293
+ acquiredAtMs: Date.now(),
294
+ ...(pidStartId ? { pidStartId } : {}),
295
+ };
296
+ }
297
+ function serializeCrossProcessLockClaim(claim) {
298
+ return `${JSON.stringify({
299
+ token: claim.token,
300
+ pid: claim.pid,
301
+ host: claim.host,
302
+ acquired_at: new Date(claim.acquiredAtMs).toISOString(),
303
+ ...(claim.pidStartId ? { pid_start_id: claim.pidStartId } : {}),
304
+ })}\n`;
305
+ }
306
+ function readCrossProcessLockState(lockPath) {
307
+ try {
308
+ const parsed = JSON.parse(readFileSync(lockPath, 'utf-8'));
309
+ const token = typeof parsed.token === 'string' && parsed.token === parsed.token.trim() && parsed.token ? parsed.token : undefined;
310
+ const pid = typeof parsed.pid === 'number' && Number.isSafeInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : undefined;
311
+ const host = typeof parsed.host === 'string' && parsed.host.trim() ? parsed.host : undefined;
312
+ const acquiredAtMs = typeof parsed.acquired_at === 'string' ? Date.parse(parsed.acquired_at) : Number.NaN;
313
+ const pidStartId = parsed.pid_start_id === undefined
314
+ ? undefined
315
+ : typeof parsed.pid_start_id === 'string' && parsed.pid_start_id.trim()
316
+ ? parsed.pid_start_id
317
+ : null;
318
+ if (!token || !pid || !host || !Number.isFinite(acquiredAtMs) || pidStartId === null)
319
+ return { kind: 'malformed' };
320
+ return {
321
+ kind: 'claim',
322
+ claim: { token, pid, host, acquiredAtMs, ...(pidStartId ? { pidStartId } : {}) },
323
+ };
324
+ }
325
+ catch (error) {
326
+ if (error.code === 'ENOENT')
327
+ return { kind: 'missing' };
328
+ return { kind: 'malformed' };
329
+ }
330
+ }
331
+ function isCrossProcessLockOwnerDead(pid) {
332
+ try {
333
+ process.kill(pid, 0);
334
+ return false;
335
+ }
336
+ catch (error) {
337
+ return error.code === 'ESRCH';
338
+ }
339
+ }
340
+ function isCrossProcessLockOlderThanLease(acquiredAtMs, nowMs) {
341
+ return acquiredAtMs < nowMs - CROSS_PROCESS_LOCK_LEASE_MS;
342
+ }
343
+ function isCrossProcessLockReclaimable(claim) {
344
+ if (claim.host !== hostname())
345
+ return isCrossProcessLockOlderThanLease(claim.acquiredAtMs, Date.now());
346
+ if (isCrossProcessLockOwnerDead(claim.pid))
347
+ return true;
348
+ const currentPidStartId = readProcessStartIdentity(claim.pid);
349
+ if (claim.pidStartId && currentPidStartId)
350
+ return currentPidStartId !== claim.pidStartId;
351
+ return isCrossProcessLockOlderThanLease(claim.acquiredAtMs, Date.now());
352
+ }
353
+ function removeCrossProcessLockFile(path) {
354
+ try {
355
+ unlinkSync(path);
356
+ }
357
+ catch (error) {
358
+ if (error.code !== 'ENOENT')
359
+ throw error;
360
+ }
361
+ }
362
+ function tryAcquireCrossProcessFileLock(lockPath, token) {
363
+ const temporaryPath = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
364
+ let acquired = false;
365
+ try {
366
+ writeFileSync(temporaryPath, serializeCrossProcessLockClaim(createCrossProcessLockClaim(token)));
367
+ try {
368
+ linkSync(temporaryPath, lockPath);
369
+ acquired = true;
370
+ return true;
371
+ }
372
+ catch (error) {
373
+ if (error.code === 'EEXIST')
374
+ return false;
375
+ throw error;
376
+ }
377
+ }
378
+ finally {
379
+ try {
380
+ removeCrossProcessLockFile(temporaryPath);
381
+ }
382
+ catch (error) {
383
+ if (!acquired)
384
+ throw error;
385
+ }
386
+ }
387
+ }
388
+ function restoreQuarantinedCrossProcessLock(lockPath, quarantinedPath) {
389
+ try {
390
+ linkSync(quarantinedPath, lockPath);
391
+ }
392
+ catch (error) {
393
+ const code = error.code;
394
+ // EEXIST: a replacement claim already holds the lock; the displaced copy is redundant.
395
+ // ENOENT: the displaced artifact was already removed (e.g. by a concurrent bounded
396
+ // sweep) — a fenced clean terminal outcome, never a cleanup failure to throw on.
397
+ if (code === 'EEXIST') {
398
+ removeCrossProcessLockFile(quarantinedPath);
399
+ return;
400
+ }
401
+ if (code === 'ENOENT')
402
+ return;
403
+ throw error;
404
+ }
405
+ removeCrossProcessLockFile(quarantinedPath);
406
+ }
407
+ function recoverCrossProcessFileLock(lockPath, observed) {
408
+ if (observed.kind === 'missing')
409
+ return true;
410
+ if (observed.kind === 'claim' && !isCrossProcessLockReclaimable(observed.claim))
411
+ return false;
412
+ const quarantinedPath = `${lockPath}.${Date.now()}.${randomUUID()}.quarantine`;
413
+ try {
414
+ renameSync(lockPath, quarantinedPath);
415
+ }
416
+ catch (error) {
417
+ if (error.code === 'ENOENT')
418
+ return true;
419
+ throw error;
420
+ }
421
+ const barrier = crossProcessQuarantineBarrier;
422
+ crossProcessQuarantineBarrier = null;
423
+ barrier?.(lockPath, quarantinedPath);
424
+ const captured = readCrossProcessLockState(quarantinedPath);
425
+ const capturedExpectedClaim = observed.kind === 'claim'
426
+ && captured.kind === 'claim'
427
+ && captured.claim.token === observed.claim.token;
428
+ const capturedRecoverable = captured.kind === 'malformed'
429
+ || (capturedExpectedClaim && captured.kind === 'claim' && isCrossProcessLockReclaimable(captured.claim));
430
+ if (!capturedRecoverable) {
431
+ restoreQuarantinedCrossProcessLock(lockPath, quarantinedPath);
432
+ return true;
433
+ }
434
+ removeCrossProcessLockFile(quarantinedPath);
435
+ return true;
436
+ }
437
+ function assertCrossProcessFileLockOwnership(lockPath, token) {
438
+ const state = readCrossProcessLockState(lockPath);
439
+ if (state.kind === 'claim' && state.claim.token === token)
440
+ return;
441
+ throw new CrossProcessLockLostError(lockPath);
442
+ }
443
+ function releaseCrossProcessFileLock(lockPath, token) {
444
+ const observed = readCrossProcessLockState(lockPath);
445
+ if (observed.kind !== 'claim' || observed.claim.token !== token)
446
+ return;
447
+ const quarantinedPath = `${lockPath}.${Date.now()}.${randomUUID()}.release`;
448
+ try {
449
+ renameSync(lockPath, quarantinedPath);
450
+ }
451
+ catch (error) {
452
+ if (error.code === 'ENOENT')
453
+ return;
454
+ throw error;
455
+ }
456
+ const barrier = crossProcessQuarantineBarrier;
457
+ crossProcessQuarantineBarrier = null;
458
+ barrier?.(lockPath, quarantinedPath);
459
+ const captured = readCrossProcessLockState(quarantinedPath);
460
+ if (captured.kind === 'claim' && captured.claim.token === token) {
461
+ removeCrossProcessLockFile(quarantinedPath);
462
+ return;
463
+ }
464
+ restoreQuarantinedCrossProcessLock(lockPath, quarantinedPath);
465
+ }
466
+ function crossProcessLockStagePath(resourcePath, token) {
467
+ return `${resourcePath}.stage.${token}`;
468
+ }
469
+ function createCrossProcessLockStage(stagePath) {
470
+ const descriptor = openSync(stagePath, 'wx');
471
+ closeSync(descriptor);
472
+ }
473
+ function sweepForeignCrossProcessLockStages(resourcePath, token) {
474
+ const directory = dirname(resourcePath);
475
+ const stagePrefix = `${basename(resourcePath)}.stage.`;
476
+ for (const entry of readdirSync(directory)) {
477
+ if (!entry.startsWith(stagePrefix) || entry.slice(stagePrefix.length) === token)
478
+ continue;
479
+ removeCrossProcessLockFile(join(directory, entry));
480
+ }
481
+ }
482
+ export const CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP = 64;
483
+ function sweepAbandonedCrossProcessLockArtifacts(resourcePath) {
484
+ const directory = dirname(resourcePath);
485
+ const lockName = basename(crossProcessLockPath(resourcePath));
486
+ const escapedLockName = lockName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
487
+ const artifactPattern = new RegExp(`^${escapedLockName}\\.([^.]+)\\.[^.]+\\.(?:quarantine|release)$`);
488
+ const nowMs = Date.now();
489
+ const aged = [];
490
+ for (const entry of readdirSync(directory)) {
491
+ const match = artifactPattern.exec(entry);
492
+ if (!match)
493
+ continue;
494
+ const displacedAtMs = Number(match[1]);
495
+ // Only lease-aged, parseable-timestamp artifacts are eligible; fresh/live/malformed-ts
496
+ // artifacts are always preserved (never delete a live successor's in-flight evidence).
497
+ if (!Number.isFinite(displacedAtMs) || nowMs - displacedAtMs <= CROSS_PROCESS_LOCK_LEASE_MS)
498
+ continue;
499
+ aged.push({ displacedAtMs, entry });
500
+ }
501
+ // Bounded, deterministic cleanup: process oldest-first (tie-break by name) and remove at
502
+ // most CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP per acquisition, so a large backlog drains
503
+ // predictably across acquisitions instead of doing unbounded work in a single lock take.
504
+ aged.sort((a, b) => a.displacedAtMs - b.displacedAtMs || (a.entry < b.entry ? -1 : a.entry > b.entry ? 1 : 0));
505
+ for (const { entry } of aged.slice(0, CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP)) {
506
+ removeCrossProcessLockFile(join(directory, entry));
507
+ }
508
+ }
509
+ let crossProcessPublishBarrier = null;
510
+ let crossProcessQuarantineBarrier = null;
511
+ export function __setCrossProcessPublishBarrierForTest(barrier) {
512
+ crossProcessPublishBarrier = barrier;
513
+ }
514
+ export function __setCrossProcessQuarantineBarrierForTest(barrier) {
515
+ crossProcessQuarantineBarrier = barrier;
516
+ }
517
+ export function withCrossProcessFileLockSync(resourcePath, operation, options = {}) {
518
+ const lockPath = crossProcessLockPath(resourcePath);
519
+ const maxAttempts = typeof options.maxAttempts === 'number' && Number.isFinite(options.maxAttempts)
520
+ ? Math.max(1, Math.floor(options.maxAttempts))
521
+ : DEFAULT_CROSS_PROCESS_LOCK_MAX_ATTEMPTS;
522
+ const retryMs = typeof options.retryMs === 'number' && Number.isFinite(options.retryMs)
523
+ ? Math.max(1, Math.floor(options.retryMs))
524
+ : DEFAULT_CROSS_PROCESS_LOCK_RETRY_MS;
525
+ mkdirSync(dirname(lockPath), { recursive: true });
526
+ const token = randomUUID();
527
+ const stagePath = crossProcessLockStagePath(resourcePath, token);
528
+ let acquired = false;
529
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
530
+ if (tryAcquireCrossProcessFileLock(lockPath, token)) {
531
+ acquired = true;
532
+ break;
533
+ }
534
+ const recovered = recoverCrossProcessFileLock(lockPath, readCrossProcessLockState(lockPath));
535
+ if (recovered && tryAcquireCrossProcessFileLock(lockPath, token)) {
536
+ acquired = true;
537
+ break;
538
+ }
539
+ if (attempt === maxAttempts - 1) {
540
+ throw new Error(`Timed out waiting for cross-process lock at ${lockPath}`);
541
+ }
542
+ sleepForCrossProcessLockSync(Math.min(25, retryMs * 2 ** Math.min(attempt, 4)));
543
+ }
544
+ if (!acquired) {
545
+ throw new Error(`Timed out waiting for cross-process lock at ${lockPath}`);
546
+ }
547
+ try {
548
+ sweepForeignCrossProcessLockStages(resourcePath, token);
549
+ sweepAbandonedCrossProcessLockArtifacts(resourcePath);
550
+ createCrossProcessLockStage(stagePath);
551
+ return operation({
552
+ assertOwnership: () => assertCrossProcessFileLockOwnership(lockPath, token),
553
+ publish: (contents) => {
554
+ let descriptor;
555
+ try {
556
+ descriptor = openSync(stagePath, 'r+');
557
+ }
558
+ catch (error) {
559
+ if (error.code === 'ENOENT')
560
+ throw new CrossProcessLockLostError(lockPath);
561
+ throw error;
562
+ }
563
+ try {
564
+ ftruncateSync(descriptor, 0);
565
+ writeSync(descriptor, contents);
566
+ }
567
+ finally {
568
+ closeSync(descriptor);
569
+ }
570
+ const barrier = crossProcessPublishBarrier;
571
+ crossProcessPublishBarrier = null;
572
+ barrier?.();
573
+ try {
574
+ renameSync(stagePath, resourcePath);
575
+ }
576
+ catch (error) {
577
+ if (error.code === 'ENOENT')
578
+ throw new CrossProcessLockLostError(lockPath);
579
+ throw error;
580
+ }
581
+ createCrossProcessLockStage(stagePath);
582
+ },
583
+ });
584
+ }
585
+ finally {
586
+ removeCrossProcessLockFile(stagePath);
587
+ releaseCrossProcessFileLock(lockPath, token);
588
+ }
589
+ }
590
+ function readSubagentTrackingStateSync(cwd) {
591
+ const path = subagentTrackingPath(cwd);
592
+ if (!existsSync(path))
593
+ return createSubagentTrackingState();
594
+ try {
595
+ return normalizeSubagentTrackingState(JSON.parse(readFileSync(path, 'utf-8')));
596
+ }
597
+ catch {
598
+ return createSubagentTrackingState();
599
+ }
600
+ }
601
+ // #3181: strict readers for the leader-attestation security decision. A missing file is a
602
+ // legitimate empty state (fresh turn), but an existing file that fails to read/parse must
603
+ // NOT be silently treated as empty — that would let corrupt/unreadable subagent evidence
604
+ // be bypassed. Callers making a security decision (attest vs deny) use these.
605
+ function readSubagentTrackingStateSyncStrict(cwd) {
606
+ const path = subagentTrackingPath(cwd);
607
+ let raw;
608
+ try {
609
+ raw = readFileSync(path, 'utf-8');
610
+ }
611
+ catch (error) {
612
+ // Only a genuine ENOENT (no file) is a clean empty state. Any other read/access error
613
+ // (permissions, I/O, ELOOP, …) must fail closed rather than be treated as empty.
614
+ if (error?.code === 'ENOENT')
615
+ return { ok: true, state: createSubagentTrackingState() };
616
+ return { ok: false };
617
+ }
618
+ try {
619
+ return { ok: true, state: normalizeSubagentTrackingState(JSON.parse(raw)) };
620
+ }
621
+ catch {
622
+ return { ok: false };
623
+ }
624
+ }
625
+ export async function readSubagentTrackingStateStrict(cwd) {
626
+ const path = subagentTrackingPath(cwd);
627
+ let raw;
628
+ try {
629
+ raw = await readFile(path, 'utf-8');
630
+ }
631
+ catch (error) {
632
+ if (error?.code === 'ENOENT')
633
+ return { ok: true, state: createSubagentTrackingState() };
634
+ return { ok: false };
635
+ }
636
+ try {
637
+ return { ok: true, state: normalizeSubagentTrackingState(JSON.parse(raw)) };
638
+ }
639
+ catch {
640
+ return { ok: false };
641
+ }
642
+ }
643
+ function threadIsTrackedAsSubagent(state, threadId) {
644
+ const id = threadId.trim();
645
+ if (!id)
646
+ return false;
647
+ for (const session of Object.values(state.sessions)) {
648
+ if (session.threads[id]?.kind === 'subagent')
649
+ return true;
650
+ }
651
+ return false;
652
+ }
653
+ function writeSubagentTrackingStateSync(cwd, state, publish) {
654
+ const normalized = normalizeSubagentTrackingState(state);
655
+ const path = subagentTrackingPath(cwd);
656
+ mkdirSync(dirname(path), { recursive: true });
657
+ const contents = `${JSON.stringify(normalized, null, 2)}\n`;
658
+ if (publish) {
659
+ publish(contents);
660
+ return path;
661
+ }
662
+ const temporaryPath = atomicTrackingTempPath(path);
663
+ writeFileSync(temporaryPath, contents);
664
+ renameSync(temporaryPath, path);
665
+ return path;
666
+ }
179
667
  export async function readSubagentTrackingState(cwd) {
180
668
  const path = subagentTrackingPath(cwd);
181
669
  if (!existsSync(path))
@@ -191,9 +679,525 @@ export async function writeSubagentTrackingState(cwd, state) {
191
679
  const normalized = normalizeSubagentTrackingState(state);
192
680
  const path = subagentTrackingPath(cwd);
193
681
  await mkdir(dirname(path), { recursive: true });
194
- await writeFile(path, `${JSON.stringify(normalized, null, 2)}\n`);
682
+ const temporaryPath = atomicTrackingTempPath(path);
683
+ await writeFile(temporaryPath, `${JSON.stringify(normalized, null, 2)}\n`);
684
+ await rename(temporaryPath, path);
195
685
  return path;
196
686
  }
687
+ function normalizeNowMs(nowMs) {
688
+ return typeof nowMs === 'number' && Number.isFinite(nowMs) ? nowMs : Date.now();
689
+ }
690
+ function isExpiredPendingRoleIntent(intent, nowMs) {
691
+ if (intent.binding_state === 'bound')
692
+ return false;
693
+ const expiresAtMs = Date.parse(intent.expires_at);
694
+ return !Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs;
695
+ }
696
+ function pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs) {
697
+ const isCwdPartitionedStateRoot = getBaseStateDirWithSource(cwd).rootSource === 'cwd-default';
698
+ const isOwn = (intent) => (intent.origin_cwd
699
+ ? canonicalizeOriginCwd(intent.origin_cwd) === canonicalOrigin
700
+ : isCwdPartitionedStateRoot);
701
+ const shouldPruneExpired = (intent) => (isOwn(intent)
702
+ && intent.binding_state !== 'bound'
703
+ && isExpiredPendingRoleIntent(intent, nowMs));
704
+ return { isOwn, shouldPruneExpired };
705
+ }
706
+ export function isRoleIntentOwnedByCwd(cwd, intent) {
707
+ return pendingRoleIntentPredicates(cwd, canonicalizeOriginCwd(cwd), Date.now()).isOwn(intent);
708
+ }
709
+ function sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken) {
710
+ return (intent.session_id === sessionId
711
+ && intent.parent_thread_id === parentThreadId
712
+ && (correlationToken === undefined || intent.correlation_token === correlationToken));
713
+ }
714
+ export function isCanonicalCorrelationToken(value) {
715
+ return typeof value === 'string' && /^[0-9a-f]{32}$/.test(value);
716
+ }
717
+ export function isCanonicalClaimantToken(value) {
718
+ return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
719
+ }
720
+ function hasOwnBoundLogicalIntent(all, isOwn, sessionId, parentThreadId, correlationToken) {
721
+ return all.some((intent) => (isOwn(intent)
722
+ && intent.binding_state === 'bound'
723
+ && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken)));
724
+ }
725
+ function selectDominantRoleIntent(candidates, canonicalOrigin) {
726
+ return [...candidates].sort((left, right) => {
727
+ const leftIsBound = left.binding_state === 'bound';
728
+ const rightIsBound = right.binding_state === 'bound';
729
+ if (leftIsBound !== rightIsBound)
730
+ return leftIsBound ? -1 : 1;
731
+ const leftIsExactOrigin = left.origin_cwd !== undefined
732
+ && canonicalizeOriginCwd(left.origin_cwd) === canonicalOrigin;
733
+ const rightIsExactOrigin = right.origin_cwd !== undefined
734
+ && canonicalizeOriginCwd(right.origin_cwd) === canonicalOrigin;
735
+ if (leftIsExactOrigin !== rightIsExactOrigin)
736
+ return leftIsExactOrigin ? -1 : 1;
737
+ const leftStableKey = [
738
+ left.role,
739
+ left.correlation_token,
740
+ left.created_at,
741
+ left.expires_at,
742
+ left.binding_state ?? '',
743
+ left.binding_claimant_token ?? '',
744
+ left.bound_at ?? '',
745
+ left.origin_cwd ?? '',
746
+ ].join('\u0000');
747
+ const rightStableKey = [
748
+ right.role,
749
+ right.correlation_token,
750
+ right.created_at,
751
+ right.expires_at,
752
+ right.binding_state ?? '',
753
+ right.binding_claimant_token ?? '',
754
+ right.bound_at ?? '',
755
+ right.origin_cwd ?? '',
756
+ ].join('\u0000');
757
+ return leftStableKey.localeCompare(rightStableKey);
758
+ })[0] ?? null;
759
+ }
760
+ export function recordPendingRoleIntent(cwd, input) {
761
+ const role = resolveInstalledRoleName(input.role);
762
+ if (!role)
763
+ return { ok: false, reason: 'unknown_role' };
764
+ const correlationToken = input.correlationToken;
765
+ if (!isCanonicalCorrelationToken(correlationToken)) {
766
+ return { ok: false, reason: 'invalid_correlation_token' };
767
+ }
768
+ const nowMs = normalizeNowMs(input.nowMs);
769
+ const sessionId = input.sessionId.trim();
770
+ const parentThreadId = input.parentThreadId.trim();
771
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
772
+ if (canonicalOrigin === null)
773
+ return { ok: false, reason: 'invalid_origin' };
774
+ const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
775
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
776
+ const state = readSubagentTrackingStateSync(cwd);
777
+ const all = state.pending_role_intents;
778
+ if (all.some((intent) => (isOwn(intent)
779
+ && intent.session_id === sessionId
780
+ && intent.parent_thread_id === parentThreadId
781
+ && (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs))))) {
782
+ return { ok: false, reason: 'single_flight_conflict' };
783
+ }
784
+ const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) ? input.ttlMs : 10 * 60_000;
785
+ const intent = {
786
+ role,
787
+ session_id: sessionId,
788
+ parent_thread_id: parentThreadId,
789
+ correlation_token: correlationToken,
790
+ created_at: new Date(nowMs).toISOString(),
791
+ expires_at: new Date(nowMs + ttlMs).toISOString(),
792
+ // Store the canonical origin workspace so it authenticates future bind/complete/recover.
793
+ ...(canonicalOrigin ? { origin_cwd: canonicalOrigin } : {}),
794
+ };
795
+ state.pending_role_intents = [...all.filter((candidate) => !shouldPruneExpired(candidate)), intent];
796
+ context.assertOwnership();
797
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
798
+ return { ok: true, intent };
799
+ });
800
+ }
801
+ /**
802
+ * #3181 Phase 1 carrier write. Durably attest the authenticated leader thread for a
803
+ * session from native-hook-reconciled metadata. Idempotent and fail-closed: it seeds
804
+ * `leader_thread_id` + `leader_attested_at` + `leader_attest_source` only when the
805
+ * session has no conflicting leader already. A different existing leader is NEVER
806
+ * overwritten (returns `native_anchor_mismatch`) so a stale/foreign leader cannot be
807
+ * replaced from a later turn. Authority for `leaderThreadId` is the caller's native
808
+ * payload thread; it must never be derived from a native session id.
809
+ */
810
+ export function attestLeaderThread(cwd, input) {
811
+ const sessionId = input.sessionId.trim();
812
+ const leaderThreadId = input.leaderThreadId.trim();
813
+ const source = input.source.trim() || 'native';
814
+ if (!sessionId || !leaderThreadId)
815
+ return { ok: false, reason: 'native_anchor_unavailable' };
816
+ const nowMs = normalizeNowMs(input.nowMs);
817
+ const nowIso = new Date(nowMs).toISOString();
818
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
819
+ // Strict read under the lock: an unreadable/corrupt tracker denies attestation
820
+ // (fail closed) rather than silently reading empty.
821
+ const read = readSubagentTrackingStateSyncStrict(cwd);
822
+ if (!read.ok)
823
+ return { ok: false, reason: 'native_anchor_unavailable' };
824
+ const state = read.state;
825
+ // Atomic positive counter-evidence: a thread tracked as a subagent in ANY session is
826
+ // never attested as a leader. Checked under the same lock as the write so a concurrent
827
+ // child record cannot race in between a caller's pre-check and this attestation.
828
+ if (threadIsTrackedAsSubagent(state, leaderThreadId)) {
829
+ return { ok: false, reason: 'native_anchor_mismatch' };
830
+ }
831
+ const existing = state.sessions[sessionId];
832
+ const existingLeader = existing?.leader_thread_id?.trim();
833
+ if (existingLeader && existingLeader !== leaderThreadId) {
834
+ return { ok: false, reason: 'native_anchor_mismatch' };
835
+ }
836
+ if (existing && existingLeader === leaderThreadId && existing.leader_attested_at) {
837
+ return { ok: true, alreadyAttested: true };
838
+ }
839
+ const session = existing
840
+ ? { ...existing }
841
+ : { session_id: sessionId, updated_at: nowIso, threads: {} };
842
+ session.leader_thread_id = leaderThreadId;
843
+ session.leader_attested_at = nowIso;
844
+ session.leader_attest_source = source;
845
+ session.updated_at = nowIso;
846
+ state.sessions = { ...state.sessions, [sessionId]: session };
847
+ context.assertOwnership();
848
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
849
+ return { ok: true, alreadyAttested: false };
850
+ });
851
+ }
852
+ /**
853
+ * #3181 Phase 2. Single tracker-locked publish that self-heals the leader thread record
854
+ * from a durable native attestation and records the pending role intent in the same
855
+ * operation, closing the previous verify-then-record TOCTOU window. Fail-closed:
856
+ * requires a durable attested leader for the session (`native_anchor_unavailable`
857
+ * otherwise) and requires `parentThreadId` to equal the attested `leader_thread_id`
858
+ * (`native_anchor_mismatch` otherwise). `leaderThreadId` is never inferred from a
859
+ * native session id. Duplicate same-identity intents reuse the original receipt
860
+ * (idempotent), never a second leader or a second intent.
861
+ */
862
+ export function ensureLeaderAndRecordIntent(cwd, input) {
863
+ const role = resolveInstalledRoleName(input.role);
864
+ if (!role)
865
+ return { ok: false, reason: 'unknown_role' };
866
+ const correlationToken = input.correlationToken;
867
+ if (!isCanonicalCorrelationToken(correlationToken)) {
868
+ return { ok: false, reason: 'invalid_correlation_token' };
869
+ }
870
+ const nowMs = normalizeNowMs(input.nowMs);
871
+ const sessionId = input.sessionId.trim();
872
+ const parentThreadId = input.parentThreadId.trim();
873
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
874
+ if (canonicalOrigin === null)
875
+ return { ok: false, reason: 'invalid_origin' };
876
+ const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
877
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
878
+ // Strict read under the lock: an unreadable/corrupt tracker fails closed rather than
879
+ // being treated as empty state.
880
+ const read = readSubagentTrackingStateSyncStrict(cwd);
881
+ if (!read.ok)
882
+ return { ok: false, reason: 'native_anchor_unavailable' };
883
+ const state = read.state;
884
+ const session = state.sessions[sessionId];
885
+ const attestedLeader = session?.leader_thread_id?.trim();
886
+ // Fail closed unless a durable native attestation established the leader anchor.
887
+ if (!session || !attestedLeader || !session.leader_attested_at) {
888
+ return { ok: false, reason: 'native_anchor_unavailable' };
889
+ }
890
+ if (parentThreadId !== attestedLeader) {
891
+ return { ok: false, reason: 'native_anchor_mismatch' };
892
+ }
893
+ // Symmetric subagent exclusion: independent of which writer won the tracker lock, a
894
+ // leader thread that is ALSO recorded as a subagent in ANY session (even a different
895
+ // one) must never receive an adapted role intent. Re-scanned here under the intent-write
896
+ // lock so a cross-session child record committed after attestation cannot slip through.
897
+ if (threadIsTrackedAsSubagent(state, attestedLeader)) {
898
+ return { ok: false, reason: 'native_anchor_mismatch' };
899
+ }
900
+ // Single-flight, role-agnostic (preserves recordPendingRoleIntent's invariant): a
901
+ // live/bound own intent for the same (session, parent) blocks a second live intent.
902
+ // Same-identity (same role) retries reuse the original receipt (idempotent); a
903
+ // different-role request for the same live parent returns single_flight_conflict
904
+ // rather than creating a second ambiguous intent.
905
+ const all = state.pending_role_intents;
906
+ const liveOwnIntent = all.find((intent) => (isOwn(intent)
907
+ && intent.session_id === sessionId
908
+ && intent.parent_thread_id === parentThreadId
909
+ && (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs))));
910
+ if (liveOwnIntent) {
911
+ if (liveOwnIntent.role === role) {
912
+ return { ok: true, intent: liveOwnIntent, reused: true };
913
+ }
914
+ return { ok: false, reason: 'single_flight_conflict' };
915
+ }
916
+ // Self-heal the leader thread record so leader-anchor consumers see kind:"leader".
917
+ const nowIso = new Date(nowMs).toISOString();
918
+ const nextSession = { ...session, threads: { ...session.threads } };
919
+ const existingLeaderThread = nextSession.threads[attestedLeader];
920
+ if (!existingLeaderThread || existingLeaderThread.kind !== 'leader') {
921
+ nextSession.threads[attestedLeader] = {
922
+ ...(existingLeaderThread ?? {}),
923
+ thread_id: attestedLeader,
924
+ kind: 'leader',
925
+ first_seen_at: existingLeaderThread?.first_seen_at ?? nowIso,
926
+ last_seen_at: nowIso,
927
+ turn_count: existingLeaderThread?.turn_count ?? 0,
928
+ };
929
+ }
930
+ nextSession.updated_at = nowIso;
931
+ const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) ? input.ttlMs : 10 * 60_000;
932
+ const intent = {
933
+ role,
934
+ session_id: sessionId,
935
+ parent_thread_id: parentThreadId,
936
+ correlation_token: correlationToken,
937
+ created_at: nowIso,
938
+ expires_at: new Date(nowMs + ttlMs).toISOString(),
939
+ ...(canonicalOrigin ? { origin_cwd: canonicalOrigin } : {}),
940
+ };
941
+ state.sessions = { ...state.sessions, [sessionId]: nextSession };
942
+ state.pending_role_intents = [...all.filter((candidate) => !shouldPruneExpired(candidate)), intent];
943
+ context.assertOwnership();
944
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
945
+ return { ok: true, intent, reused: false };
946
+ });
947
+ }
948
+ /**
949
+ * #3181 legacy/native fallback, made atomic and positive-provenance-only. When no durable
950
+ * attestation exists, an in-turn role intent may still be authorized ONLY against the
951
+ * tracker leader_thread_id — the positively-provenanced leader thread set by a real
952
+ * recorded leader turn (notify) or by PreToolUse attestation (gated when the caller has a
953
+ * usable current pointer). It deliberately does NOT trust the reconciled session.json
954
+ * native_session_id, which an ambiguous/malformed-child SessionStart can set. This is the
955
+ * strict tracker-locked replacement for the former CLI "activeLeaderThreadIds +
956
+ * recordPendingRoleIntent" fallback: it validates the leader anchor AND rejects any
957
+ * all-session subagent counter-evidence under the same lock as the intent write, so a
958
+ * subagent record landing after a pre-check (or a PreToolUse attestation that lost its
959
+ * race) can never be downgraded into an unvalidated legacy authorization. Fail-closed:
960
+ * unreadable/corrupt tracker → native_anchor_unavailable; parent not the tracker leader →
961
+ * parent_not_active_leader; parent tracked as a subagent anywhere → native_anchor_mismatch.
962
+ */
963
+ export function recordNativeLeaderIntent(cwd, input) {
964
+ const role = resolveInstalledRoleName(input.role);
965
+ if (!role)
966
+ return { ok: false, reason: 'unknown_role' };
967
+ const correlationToken = input.correlationToken;
968
+ if (!isCanonicalCorrelationToken(correlationToken)) {
969
+ return { ok: false, reason: 'invalid_correlation_token' };
970
+ }
971
+ const nowMs = normalizeNowMs(input.nowMs);
972
+ const sessionId = input.sessionId.trim();
973
+ const parentThreadId = input.parentThreadId.trim();
974
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
975
+ if (canonicalOrigin === null)
976
+ return { ok: false, reason: 'invalid_origin' };
977
+ const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
978
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
979
+ const read = readSubagentTrackingStateSyncStrict(cwd);
980
+ if (!read.ok)
981
+ return { ok: false, reason: 'native_anchor_unavailable' };
982
+ const state = read.state;
983
+ // Positive-provenance leader anchor ONLY: the tracker leader_thread_id is set by a real
984
+ // recorded leader turn (notify) or by PreToolUse attestation. It is intentionally NOT
985
+ // the reconciled session.json native_session_id, which an ambiguous/malformed-child
986
+ // SessionStart can set without positively classifying a root — trusting that pointer
987
+ // would re-open false-leader adoption. A malformed child never obtains a tracker leader
988
+ // thread, so it fails closed here.
989
+ const trackerLeader = input.allowTrackerLeader ? state.sessions[sessionId]?.leader_thread_id?.trim() : undefined;
990
+ if (!parentThreadId || !trackerLeader || parentThreadId !== trackerLeader) {
991
+ return { ok: false, reason: 'parent_not_active_leader' };
992
+ }
993
+ // Atomic all-session subagent exclusion: a parent thread recorded as a subagent in ANY
994
+ // session is never a valid leader, even if it matches the tracker leader anchor.
995
+ if (threadIsTrackedAsSubagent(state, parentThreadId)) {
996
+ return { ok: false, reason: 'native_anchor_mismatch' };
997
+ }
998
+ // Role-agnostic single-flight (same contract as recordPendingRoleIntent).
999
+ const all = state.pending_role_intents;
1000
+ const liveOwnIntent = all.find((intent) => (isOwn(intent)
1001
+ && intent.session_id === sessionId
1002
+ && intent.parent_thread_id === parentThreadId
1003
+ && (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs))));
1004
+ if (liveOwnIntent) {
1005
+ if (liveOwnIntent.role === role) {
1006
+ return { ok: true, intent: liveOwnIntent, reused: true };
1007
+ }
1008
+ return { ok: false, reason: 'single_flight_conflict' };
1009
+ }
1010
+ const nowIso = new Date(nowMs).toISOString();
1011
+ const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) ? input.ttlMs : 10 * 60_000;
1012
+ const intent = {
1013
+ role,
1014
+ session_id: sessionId,
1015
+ parent_thread_id: parentThreadId,
1016
+ correlation_token: correlationToken,
1017
+ created_at: nowIso,
1018
+ expires_at: new Date(nowMs + ttlMs).toISOString(),
1019
+ ...(canonicalOrigin ? { origin_cwd: canonicalOrigin } : {}),
1020
+ };
1021
+ state.pending_role_intents = [...all.filter((candidate) => !shouldPruneExpired(candidate)), intent];
1022
+ context.assertOwnership();
1023
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1024
+ return { ok: true, intent, reused: false };
1025
+ });
1026
+ }
1027
+ export function bindPendingRoleIntentUnderLock(cwd, input, bind) {
1028
+ const nowMs = normalizeNowMs(input.nowMs);
1029
+ const sessionId = input.sessionId.trim();
1030
+ const parentThreadId = input.parentThreadId.trim();
1031
+ const correlationToken = input.correlationToken;
1032
+ if (!isCanonicalCorrelationToken(correlationToken))
1033
+ return null;
1034
+ // Fail-closed origin authentication: establish the caller's canonical origin workspace up
1035
+ // front. A malformed/unavailable origin can never disclose role/claimant, run the bind
1036
+ // callback, mutate pending->bound, or acquire the lock.
1037
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
1038
+ if (canonicalOrigin === null)
1039
+ return null;
1040
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
1041
+ const state = readSubagentTrackingStateSync(cwd);
1042
+ const all = state.pending_role_intents;
1043
+ const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
1044
+ const matchedIntent = selectDominantRoleIntent(all.filter((intent) => (isOwn(intent)
1045
+ && correlationToken !== undefined
1046
+ && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken)
1047
+ && (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs)))), canonicalOrigin);
1048
+ if (!matchedIntent) {
1049
+ const retained = all.filter((intent) => !shouldPruneExpired(intent));
1050
+ if (retained.length !== all.length) {
1051
+ state.pending_role_intents = retained;
1052
+ context.assertOwnership();
1053
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1054
+ }
1055
+ return null;
1056
+ }
1057
+ const adaptedIntent = {
1058
+ role: matchedIntent.role,
1059
+ provenanceKind: OMX_ADAPTED_PROVENANCE,
1060
+ };
1061
+ if (matchedIntent.binding_state === 'bound') {
1062
+ const next = all
1063
+ .filter((intent) => (intent.binding_state === 'bound'
1064
+ || !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))))
1065
+ .map((intent) => (intent === matchedIntent && !intent.origin_cwd
1066
+ ? { ...intent, origin_cwd: canonicalOrigin }
1067
+ : intent));
1068
+ if (next.length !== all.length || !matchedIntent.origin_cwd) {
1069
+ state.pending_role_intents = next;
1070
+ context.assertOwnership();
1071
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1072
+ }
1073
+ return {
1074
+ ...adaptedIntent,
1075
+ claimantToken: undefined,
1076
+ alreadyBound: true,
1077
+ };
1078
+ }
1079
+ const claimantToken = randomUUID();
1080
+ const boundState = bind(state, adaptedIntent);
1081
+ boundState.pending_role_intents = all
1082
+ .filter((intent) => !shouldPruneExpired(intent))
1083
+ .filter((intent) => (intent === matchedIntent
1084
+ || !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))))
1085
+ .map((intent) => (intent === matchedIntent
1086
+ ? {
1087
+ ...matchedIntent,
1088
+ binding_state: 'bound',
1089
+ binding_claimant_token: claimantToken,
1090
+ bound_at: new Date(nowMs).toISOString(),
1091
+ origin_cwd: matchedIntent.origin_cwd ?? canonicalOrigin,
1092
+ }
1093
+ : intent));
1094
+ context.assertOwnership();
1095
+ writeSubagentTrackingStateSync(cwd, boundState, context.publish);
1096
+ return { ...adaptedIntent, claimantToken, alreadyBound: false };
1097
+ });
1098
+ }
1099
+ export function consumePendingRoleIntent(cwd, input) {
1100
+ const nowMs = normalizeNowMs(input.nowMs);
1101
+ const sessionId = input.sessionId.trim();
1102
+ const parentThreadId = input.parentThreadId.trim();
1103
+ const correlationToken = input.correlationToken;
1104
+ if (!isCanonicalCorrelationToken(correlationToken))
1105
+ return null;
1106
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
1107
+ if (canonicalOrigin === null)
1108
+ return null;
1109
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
1110
+ const state = readSubagentTrackingStateSync(cwd);
1111
+ const all = state.pending_role_intents;
1112
+ const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
1113
+ if (hasOwnBoundLogicalIntent(all, isOwn, sessionId, parentThreadId, correlationToken))
1114
+ return null;
1115
+ const consumed = selectDominantRoleIntent(all.filter((intent) => (isOwn(intent)
1116
+ && intent.binding_state !== 'bound'
1117
+ && !isExpiredPendingRoleIntent(intent, nowMs)
1118
+ && correlationToken !== undefined
1119
+ && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))), canonicalOrigin);
1120
+ if (!consumed) {
1121
+ const retained = all.filter((intent) => !shouldPruneExpired(intent));
1122
+ if (retained.length !== all.length) {
1123
+ state.pending_role_intents = retained;
1124
+ context.assertOwnership();
1125
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1126
+ }
1127
+ return null;
1128
+ }
1129
+ state.pending_role_intents = all
1130
+ .filter((intent) => !shouldPruneExpired(intent))
1131
+ .filter((intent) => (intent.binding_state === 'bound'
1132
+ || !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))));
1133
+ context.assertOwnership();
1134
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1135
+ return { role: consumed.role, provenanceKind: OMX_ADAPTED_PROVENANCE };
1136
+ });
1137
+ }
1138
+ export function completeAdaptedRoleBinding(cwd, input) {
1139
+ const nowMs = normalizeNowMs(input.nowMs);
1140
+ const sessionId = input.sessionId.trim();
1141
+ const parentThreadId = input.parentThreadId.trim();
1142
+ const correlationToken = input.correlationToken;
1143
+ const claimantToken = input.claimantToken;
1144
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
1145
+ if (canonicalOrigin === null)
1146
+ return 'not_found';
1147
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
1148
+ const state = readSubagentTrackingStateSync(cwd);
1149
+ const all = state.pending_role_intents;
1150
+ const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
1151
+ // Select the owned bound scope before authenticating it. Credentials are not a lookup key:
1152
+ // otherwise an invalid dominant duplicate could be bypassed by a lower valid duplicate.
1153
+ const boundIntent = selectDominantRoleIntent(all.filter((intent) => (isOwn(intent)
1154
+ && intent.binding_state === 'bound'
1155
+ && sameLogicalRoleIntent(intent, sessionId, parentThreadId))), canonicalOrigin);
1156
+ if (!boundIntent) {
1157
+ const retained = all.filter((intent) => !shouldPruneExpired(intent));
1158
+ if (retained.length !== all.length) {
1159
+ state.pending_role_intents = retained;
1160
+ context.assertOwnership();
1161
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1162
+ }
1163
+ return 'not_found';
1164
+ }
1165
+ const hasClaimant = Object.hasOwn(boundIntent, 'binding_claimant_token');
1166
+ if (!isCanonicalCorrelationToken(boundIntent.correlation_token)
1167
+ || !isCanonicalCorrelationToken(correlationToken)
1168
+ || boundIntent.correlation_token !== correlationToken
1169
+ || (hasClaimant && (!isCanonicalClaimantToken(boundIntent.binding_claimant_token)
1170
+ || !isCanonicalClaimantToken(claimantToken)
1171
+ || boundIntent.binding_claimant_token !== claimantToken)))
1172
+ return 'claimant_mismatch';
1173
+ state.pending_role_intents = all
1174
+ .filter((intent) => !shouldPruneExpired(intent))
1175
+ .filter((intent) => !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, boundIntent.correlation_token)));
1176
+ context.assertOwnership();
1177
+ writeSubagentTrackingStateSync(cwd, state, context.publish);
1178
+ return 'completed';
1179
+ });
1180
+ }
1181
+ export function listBoundAdaptedRoleIntents(cwd, _nowMs, ownedDominant = false) {
1182
+ const allBound = readSubagentTrackingStateSync(cwd).pending_role_intents.filter((intent) => intent.binding_state === 'bound');
1183
+ if (!ownedDominant)
1184
+ return allBound;
1185
+ const canonicalOrigin = canonicalizeOriginCwd(cwd);
1186
+ if (canonicalOrigin === null)
1187
+ return [];
1188
+ const { isOwn } = pendingRoleIntentPredicates(cwd, canonicalOrigin, Date.now());
1189
+ const scopes = new Map();
1190
+ for (const intent of allBound) {
1191
+ if (!isOwn(intent))
1192
+ continue;
1193
+ const key = `${intent.session_id}\u0000${intent.parent_thread_id}`;
1194
+ scopes.set(key, [...(scopes.get(key) ?? []), intent]);
1195
+ }
1196
+ return [...scopes.values()].flatMap((candidates) => {
1197
+ const dominant = selectDominantRoleIntent(candidates, canonicalOrigin);
1198
+ return dominant ? [dominant] : [];
1199
+ });
1200
+ }
197
1201
  export function recordSubagentTurn(state, input) {
198
1202
  const sessionId = input.sessionId.trim();
199
1203
  const threadId = input.threadId.trim();
@@ -209,42 +1213,29 @@ export function recordSubagentTurn(state, input) {
209
1213
  const requestedKind = input.kind === 'leader' || input.kind === 'subagent' ? input.kind : undefined;
210
1214
  const requestedLeaderThreadId = input.leaderThreadId?.trim();
211
1215
  const existingThread = existingSession.threads[threadId];
212
- const existingKind = existingThread?.kind === 'leader' || existingThread?.kind === 'subagent'
213
- ? existingThread.kind
214
- : undefined;
1216
+ const existingKind = existingThread?.kind === 'leader' || existingThread?.kind === 'subagent' ? existingThread.kind : undefined;
215
1217
  const existingLeaderThreadId = existingSession.leader_thread_id?.trim();
216
1218
  // `leader_thread_id` is the session's top-level leader boundary. A native
217
1219
  // subagent can itself be the immediate parent of a nested native role, but
218
1220
  // that must not reclassify known subagent evidence as the session leader.
219
- const requestedLeaderThread = requestedLeaderThreadId
220
- ? existingSession.threads[requestedLeaderThreadId]
221
- : undefined;
1221
+ const requestedLeaderThread = requestedLeaderThreadId ? existingSession.threads[requestedLeaderThreadId] : undefined;
222
1222
  const requestedLeaderWouldReclassifySubagent = requestedLeaderThread?.kind === 'subagent';
223
- const requestedSessionLeaderThreadId = requestedLeaderWouldReclassifySubagent
224
- ? undefined
225
- : requestedLeaderThreadId;
1223
+ const requestedSessionLeaderThreadId = requestedLeaderWouldReclassifySubagent ? undefined : requestedLeaderThreadId;
226
1224
  const preserveExistingSubagent = existingKind === 'subagent' && requestedKind !== 'subagent';
227
- const preserveKnownLeader = requestedKind === 'subagent'
228
- && (existingKind === 'leader' || existingLeaderThreadId === threadId);
1225
+ const preserveKnownLeader = requestedKind === 'subagent' && (existingKind === 'leader' || existingLeaderThreadId === threadId);
229
1226
  const leaderThreadId = preserveKnownLeader
230
1227
  ? existingLeaderThreadId || threadId
231
- : existingLeaderThreadId
232
- || requestedSessionLeaderThreadId
233
- || (requestedKind === 'subagent' || preserveExistingSubagent ? undefined : threadId);
1228
+ : existingLeaderThreadId || requestedSessionLeaderThreadId || (requestedKind === 'subagent' || preserveExistingSubagent ? undefined : threadId);
234
1229
  const kind = preserveKnownLeader
235
1230
  ? 'leader'
236
1231
  : requestedKind === 'leader' && existingKind === 'subagent'
237
1232
  ? 'subagent'
238
- : requestedKind ?? (threadId === leaderThreadId ? 'leader' : existingKind ?? 'subagent');
1233
+ : (requestedKind ?? (threadId === leaderThreadId ? 'leader' : (existingKind ?? 'subagent')));
239
1234
  const requestedStatus = normalizeSubagentStatus(input.status);
240
1235
  const preservedStatus = normalizeSubagentStatus(existingThread?.status);
241
1236
  const preserveCompletionEvidence = input.preserveCompletionEvidence === true;
242
- const clearsPriorCompletion = input.completed !== true
243
- && preserveCompletionEvidence !== true
244
- && Boolean(existingThread?.completed_at);
245
- const status = requestedStatus
246
- ?? (input.completed ? 'closed' : undefined)
247
- ?? (clearsPriorCompletion ? undefined : preservedStatus);
1237
+ const clearsPriorCompletion = input.completed !== true && preserveCompletionEvidence !== true && Boolean(existingThread?.completed_at);
1238
+ const status = requestedStatus ?? (input.completed ? 'closed' : undefined) ?? (clearsPriorCompletion ? undefined : preservedStatus);
248
1239
  const preservedCompletion = preserveCompletionEvidence && existingThread?.completed_at
249
1240
  ? {
250
1241
  completed_at: existingThread.completed_at,
@@ -258,7 +1249,11 @@ export function recordSubagentTurn(state, input) {
258
1249
  first_seen_at: existingThread?.first_seen_at ?? timestamp,
259
1250
  last_seen_at: timestamp,
260
1251
  turn_count: (existingThread?.turn_count ?? 0) + 1,
261
- ...(input.turnId?.trim() ? { last_turn_id: input.turnId.trim() } : existingThread?.last_turn_id ? { last_turn_id: existingThread.last_turn_id } : {}),
1252
+ ...(input.turnId?.trim()
1253
+ ? { last_turn_id: input.turnId.trim() }
1254
+ : existingThread?.last_turn_id
1255
+ ? { last_turn_id: existingThread.last_turn_id }
1256
+ : {}),
262
1257
  ...(input.completed
263
1258
  ? {
264
1259
  completed_at: timestamp,
@@ -268,27 +1263,44 @@ export function recordSubagentTurn(state, input) {
268
1263
  : preservedCompletion),
269
1264
  ...(input.mode?.trim() ? { mode: input.mode.trim() } : existingThread?.mode ? { mode: existingThread.mode } : {}),
270
1265
  ...(input.role?.trim() ? { role: input.role.trim() } : existingThread?.role ? { role: existingThread.role } : {}),
1266
+ ...(input.provenanceKind?.trim()
1267
+ ? { provenance_kind: input.provenanceKind.trim() }
1268
+ : existingThread?.provenance_kind
1269
+ ? { provenance_kind: existingThread.provenance_kind }
1270
+ : {}),
271
1271
  ...(input.laneId?.trim() ? { lane_id: input.laneId.trim() } : existingThread?.lane_id ? { lane_id: existingThread.lane_id } : {}),
272
1272
  ...(input.scope?.trim() ? { scope: input.scope.trim() } : existingThread?.scope ? { scope: existingThread.scope } : {}),
273
1273
  ...(input.agentNickname?.trim()
274
1274
  ? { agent_nickname: input.agentNickname.trim() }
275
- : existingThread?.agent_nickname ? { agent_nickname: existingThread.agent_nickname } : {}),
1275
+ : existingThread?.agent_nickname
1276
+ ? { agent_nickname: existingThread.agent_nickname }
1277
+ : {}),
276
1278
  ...(status ? { status } : {}),
277
1279
  ...(input.lastHandoffSummary?.trim()
278
1280
  ? { last_handoff_summary: input.lastHandoffSummary.trim() }
279
- : existingThread?.last_handoff_summary ? { last_handoff_summary: existingThread.last_handoff_summary } : {}),
1281
+ : existingThread?.last_handoff_summary
1282
+ ? { last_handoff_summary: existingThread.last_handoff_summary }
1283
+ : {}),
280
1284
  ...(input.resumeRequestedAt?.trim()
281
1285
  ? { resume_requested_at: input.resumeRequestedAt.trim() }
282
- : existingThread?.resume_requested_at ? { resume_requested_at: existingThread.resume_requested_at } : {}),
1286
+ : existingThread?.resume_requested_at
1287
+ ? { resume_requested_at: existingThread.resume_requested_at }
1288
+ : {}),
283
1289
  ...(input.resumeCompletedAt?.trim()
284
1290
  ? { resume_completed_at: input.resumeCompletedAt.trim() }
285
- : existingThread?.resume_completed_at ? { resume_completed_at: existingThread.resume_completed_at } : {}),
1291
+ : existingThread?.resume_completed_at
1292
+ ? { resume_completed_at: existingThread.resume_completed_at }
1293
+ : {}),
286
1294
  ...(input.resumeFailedAt?.trim()
287
1295
  ? { resume_failed_at: input.resumeFailedAt.trim() }
288
- : existingThread?.resume_failed_at ? { resume_failed_at: existingThread.resume_failed_at } : {}),
1296
+ : existingThread?.resume_failed_at
1297
+ ? { resume_failed_at: existingThread.resume_failed_at }
1298
+ : {}),
289
1299
  ...(input.resumeFailureReason?.trim()
290
1300
  ? { resume_failure_reason: input.resumeFailureReason.trim() }
291
- : existingThread?.resume_failure_reason ? { resume_failure_reason: existingThread.resume_failure_reason } : {}),
1301
+ : existingThread?.resume_failure_reason
1302
+ ? { resume_failure_reason: existingThread.resume_failure_reason }
1303
+ : {}),
292
1304
  };
293
1305
  const threads = {
294
1306
  ...existingSession.threads,
@@ -303,16 +1315,24 @@ export function recordSubagentTurn(state, input) {
303
1315
  normalized.sessions[sessionId] = {
304
1316
  session_id: sessionId,
305
1317
  ...(leaderThreadId ? { leader_thread_id: leaderThreadId } : {}),
1318
+ // #3181: preserve the durable native leader attestation across ordinary turn writes.
1319
+ // Dropping these would let a normal child SessionStart/bind erase authentication and
1320
+ // silently downgrade later role-intent writes to the legacy (non-atomic) path.
1321
+ ...(existingSession.leader_attested_at ? { leader_attested_at: existingSession.leader_attested_at } : {}),
1322
+ ...(existingSession.leader_attest_source ? { leader_attest_source: existingSession.leader_attest_source } : {}),
306
1323
  updated_at: timestamp,
307
1324
  threads,
308
1325
  };
309
1326
  return normalized;
310
1327
  }
311
1328
  export async function recordSubagentTurnForSession(cwd, input) {
312
- const current = await readSubagentTrackingState(cwd);
313
- const next = recordSubagentTurn(current, input);
314
- await writeSubagentTrackingState(cwd, next);
315
- return next;
1329
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
1330
+ const current = readSubagentTrackingStateSync(cwd);
1331
+ const next = recordSubagentTurn(current, input);
1332
+ context.assertOwnership();
1333
+ writeSubagentTrackingStateSync(cwd, next, context.publish);
1334
+ return next;
1335
+ });
316
1336
  }
317
1337
  export function summarizeSubagentSession(state, sessionId, options = {}) {
318
1338
  const normalized = normalizeSubagentTrackingState(state);
@@ -320,11 +1340,7 @@ export function summarizeSubagentSession(state, sessionId, options = {}) {
320
1340
  if (!session)
321
1341
  return null;
322
1342
  const activeWindowMs = options.activeWindowMs ?? DEFAULT_SUBAGENT_ACTIVE_WINDOW_MS;
323
- const nowMs = typeof options.now === 'string'
324
- ? Date.parse(options.now)
325
- : options.now instanceof Date
326
- ? options.now.getTime()
327
- : Date.now();
1343
+ const nowMs = typeof options.now === 'string' ? Date.parse(options.now) : options.now instanceof Date ? options.now.getTime() : Date.now();
328
1344
  const allThreadIds = Object.keys(session.threads).sort();
329
1345
  const allSubagentThreadIds = allThreadIds.filter((threadId) => isTrustedSubagentThread(session, threadId));
330
1346
  const activeSubagentThreadIds = allSubagentThreadIds.filter((threadId) => {