oh-my-codex 0.20.2 → 0.20.5

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 (805) hide show
  1. package/Cargo.lock +36 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +28 -2
  4. package/crates/omx-runtime/Cargo.toml +4 -0
  5. package/crates/omx-runtime/src/main.rs +415 -2
  6. package/crates/omx-runtime/tests/execution.rs +483 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  11. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  12. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  13. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  14. package/dist/adapt/contracts.d.ts +1 -1
  15. package/dist/adapt/contracts.d.ts.map +1 -1
  16. package/dist/adapt/contracts.js +1 -1
  17. package/dist/adapt/contracts.js.map +1 -1
  18. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  19. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  20. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  21. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  23. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  24. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  25. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  27. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  28. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  29. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  31. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  32. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  33. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  35. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  36. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  37. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  39. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  40. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  41. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  43. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  44. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  45. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  47. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  48. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  49. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  51. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  52. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  53. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  54. package/dist/adapt/herdr/authority.d.ts +34 -0
  55. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  56. package/dist/adapt/herdr/authority.js +71 -0
  57. package/dist/adapt/herdr/authority.js.map +1 -0
  58. package/dist/adapt/herdr/bridge.d.ts +84 -0
  59. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  60. package/dist/adapt/herdr/bridge.js +222 -0
  61. package/dist/adapt/herdr/bridge.js.map +1 -0
  62. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  63. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  64. package/dist/adapt/herdr/sanitize.js +70 -0
  65. package/dist/adapt/herdr/sanitize.js.map +1 -0
  66. package/dist/adapt/herdr/semantic.d.ts +67 -0
  67. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  68. package/dist/adapt/herdr/semantic.js +170 -0
  69. package/dist/adapt/herdr/semantic.js.map +1 -0
  70. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  71. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  72. package/dist/adapt/herdr/seq-store.js +126 -0
  73. package/dist/adapt/herdr/seq-store.js.map +1 -0
  74. package/dist/adapt/herdr/team-map.d.ts +30 -0
  75. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  76. package/dist/adapt/herdr/team-map.js +37 -0
  77. package/dist/adapt/herdr/team-map.js.map +1 -0
  78. package/dist/adapt/herdr/transport.d.ts +115 -0
  79. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  80. package/dist/adapt/herdr/transport.js +260 -0
  81. package/dist/adapt/herdr/transport.js.map +1 -0
  82. package/dist/adapt/herdr/wiring.d.ts +24 -0
  83. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  84. package/dist/adapt/herdr/wiring.js +62 -0
  85. package/dist/adapt/herdr/wiring.js.map +1 -0
  86. package/dist/adapt/herdr.d.ts +19 -0
  87. package/dist/adapt/herdr.d.ts.map +1 -0
  88. package/dist/adapt/herdr.js +115 -0
  89. package/dist/adapt/herdr.js.map +1 -0
  90. package/dist/adapt/index.d.ts +4 -0
  91. package/dist/adapt/index.d.ts.map +1 -1
  92. package/dist/adapt/index.js +48 -0
  93. package/dist/adapt/index.js.map +1 -1
  94. package/dist/adapt/registry.d.ts.map +1 -1
  95. package/dist/adapt/registry.js +11 -0
  96. package/dist/adapt/registry.js.map +1 -1
  97. package/dist/agents/__tests__/definitions.test.js +81 -0
  98. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  99. package/dist/agents/__tests__/native-config.test.js +25 -0
  100. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  101. package/dist/agents/native-config.d.ts +2 -1
  102. package/dist/agents/native-config.d.ts.map +1 -1
  103. package/dist/agents/native-config.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +22 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +107 -22
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  115. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  116. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  117. package/dist/autopilot/ralplan-gate.js +7 -6
  118. package/dist/autopilot/ralplan-gate.js.map +1 -1
  119. package/dist/cli/__tests__/agents-init.test.js +4 -23
  120. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  121. package/dist/cli/__tests__/api.test.js +61 -24
  122. package/dist/cli/__tests__/api.test.js.map +1 -1
  123. package/dist/cli/__tests__/auth.test.js +63 -7
  124. package/dist/cli/__tests__/auth.test.js.map +1 -1
  125. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  126. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  127. package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
  128. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  129. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  130. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  131. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  132. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  133. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  134. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  135. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  136. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  137. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  138. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  139. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  140. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  141. package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
  142. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  143. package/dist/cli/__tests__/exec.test.js +211 -0
  144. package/dist/cli/__tests__/exec.test.js.map +1 -1
  145. package/dist/cli/__tests__/index.test.js +878 -463
  146. package/dist/cli/__tests__/index.test.js.map +1 -1
  147. package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
  148. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  149. package/dist/cli/__tests__/mcp-parity.test.js +8 -4
  150. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  151. package/dist/cli/__tests__/native-assets.test.js +398 -16
  152. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  153. package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
  154. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  155. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  156. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  157. package/dist/cli/__tests__/ralph.test.js +3 -3
  158. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  159. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
  160. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  161. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  162. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  163. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  164. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  165. package/dist/cli/__tests__/ralplan.test.js +107 -109
  166. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  167. package/dist/cli/__tests__/resume.test.js +11 -10
  168. package/dist/cli/__tests__/resume.test.js.map +1 -1
  169. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  170. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  171. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  172. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  173. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  174. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  175. package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
  176. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  177. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  178. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  179. package/dist/cli/__tests__/team.test.js +150 -8
  180. package/dist/cli/__tests__/team.test.js.map +1 -1
  181. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  182. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  183. package/dist/cli/__tests__/uninstall.test.js +95 -4
  184. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  185. package/dist/cli/__tests__/update.test.js +669 -177
  186. package/dist/cli/__tests__/update.test.js.map +1 -1
  187. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
  188. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  189. package/dist/cli/api.d.ts.map +1 -1
  190. package/dist/cli/api.js +9 -10
  191. package/dist/cli/api.js.map +1 -1
  192. package/dist/cli/codex-feature-probe.d.ts +16 -0
  193. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  194. package/dist/cli/codex-feature-probe.js +44 -0
  195. package/dist/cli/codex-feature-probe.js.map +1 -1
  196. package/dist/cli/codex-home.d.ts.map +1 -1
  197. package/dist/cli/codex-home.js +7 -4
  198. package/dist/cli/codex-home.js.map +1 -1
  199. package/dist/cli/doctor.d.ts +20 -0
  200. package/dist/cli/doctor.d.ts.map +1 -1
  201. package/dist/cli/doctor.js +290 -3
  202. package/dist/cli/doctor.js.map +1 -1
  203. package/dist/cli/index.d.ts +234 -12
  204. package/dist/cli/index.d.ts.map +1 -1
  205. package/dist/cli/index.js +2409 -746
  206. package/dist/cli/index.js.map +1 -1
  207. package/dist/cli/mcp-parity.js +2 -2
  208. package/dist/cli/mcp-parity.js.map +1 -1
  209. package/dist/cli/native-assets.d.ts +25 -0
  210. package/dist/cli/native-assets.d.ts.map +1 -1
  211. package/dist/cli/native-assets.js +448 -51
  212. package/dist/cli/native-assets.js.map +1 -1
  213. package/dist/cli/native-hook-claim-journal.d.ts +20 -5
  214. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  215. package/dist/cli/native-hook-claim-journal.js +67 -32
  216. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  217. package/dist/cli/package-manager-ownership.d.ts +63 -0
  218. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  219. package/dist/cli/package-manager-ownership.js +323 -0
  220. package/dist/cli/package-manager-ownership.js.map +1 -0
  221. package/dist/cli/ralph.js +2 -2
  222. package/dist/cli/ralph.js.map +1 -1
  223. package/dist/cli/ralplan.d.ts +5 -7
  224. package/dist/cli/ralplan.d.ts.map +1 -1
  225. package/dist/cli/ralplan.js +83 -134
  226. package/dist/cli/ralplan.js.map +1 -1
  227. package/dist/cli/session-search.d.ts +5 -0
  228. package/dist/cli/session-search.d.ts.map +1 -1
  229. package/dist/cli/session-search.js +87 -0
  230. package/dist/cli/session-search.js.map +1 -1
  231. package/dist/cli/setup-preferences.d.ts +15 -0
  232. package/dist/cli/setup-preferences.d.ts.map +1 -1
  233. package/dist/cli/setup-preferences.js +27 -1
  234. package/dist/cli/setup-preferences.js.map +1 -1
  235. package/dist/cli/setup.d.ts +8 -0
  236. package/dist/cli/setup.d.ts.map +1 -1
  237. package/dist/cli/setup.js +119 -47
  238. package/dist/cli/setup.js.map +1 -1
  239. package/dist/cli/sparkshell.d.ts.map +1 -1
  240. package/dist/cli/sparkshell.js +52 -9
  241. package/dist/cli/sparkshell.js.map +1 -1
  242. package/dist/cli/ultragoal.d.ts.map +1 -1
  243. package/dist/cli/ultragoal.js +2 -2
  244. package/dist/cli/ultragoal.js.map +1 -1
  245. package/dist/cli/uninstall.d.ts +9 -0
  246. package/dist/cli/uninstall.d.ts.map +1 -1
  247. package/dist/cli/uninstall.js +71 -30
  248. package/dist/cli/uninstall.js.map +1 -1
  249. package/dist/cli/update-worker.d.ts +2 -0
  250. package/dist/cli/update-worker.d.ts.map +1 -0
  251. package/dist/cli/update-worker.js +142 -0
  252. package/dist/cli/update-worker.js.map +1 -0
  253. package/dist/cli/update.d.ts +14 -22
  254. package/dist/cli/update.d.ts.map +1 -1
  255. package/dist/cli/update.js +192 -241
  256. package/dist/cli/update.js.map +1 -1
  257. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  258. package/dist/config/__tests__/generator-idempotent.test.js +503 -30
  259. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  260. package/dist/config/__tests__/generator-notify.test.js +5 -3
  261. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  262. package/dist/config/__tests__/models.test.js +67 -3
  263. package/dist/config/__tests__/models.test.js.map +1 -1
  264. package/dist/config/generator.d.ts +27 -21
  265. package/dist/config/generator.d.ts.map +1 -1
  266. package/dist/config/generator.js +610 -152
  267. package/dist/config/generator.js.map +1 -1
  268. package/dist/config/models.d.ts +12 -2
  269. package/dist/config/models.d.ts.map +1 -1
  270. package/dist/config/models.js +15 -1
  271. package/dist/config/models.js.map +1 -1
  272. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  273. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  274. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  275. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  276. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  277. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  278. package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
  279. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  280. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  281. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  282. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  283. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  284. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  285. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  286. package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
  287. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  288. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
  289. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  290. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
  291. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  292. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
  293. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  294. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  295. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  296. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  297. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  298. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  299. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  300. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
  301. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  302. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
  303. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  304. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  305. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  306. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  307. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  308. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  309. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  310. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  311. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  312. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
  313. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  314. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  315. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  316. package/dist/hooks/__tests__/session.test.js +3130 -365
  317. package/dist/hooks/__tests__/session.test.js.map +1 -1
  318. package/dist/hooks/agents-overlay.js +1 -1
  319. package/dist/hooks/agents-overlay.js.map +1 -1
  320. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  321. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  322. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  323. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  324. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  325. package/dist/hooks/extensibility/dispatcher.js +20 -3
  326. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  327. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  328. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  329. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  330. package/dist/hooks/extensibility/runtime.js.map +1 -1
  331. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  332. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  333. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  334. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  335. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  336. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  337. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  338. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  339. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  340. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  341. package/dist/hooks/extensibility/sdk.js +1 -1
  342. package/dist/hooks/extensibility/sdk.js.map +1 -1
  343. package/dist/hooks/extensibility/types.d.ts +7 -0
  344. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  345. package/dist/hooks/keyword-detector.d.ts +10 -3
  346. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  347. package/dist/hooks/keyword-detector.js +116 -20
  348. package/dist/hooks/keyword-detector.js.map +1 -1
  349. package/dist/hooks/session.d.ts +204 -14
  350. package/dist/hooks/session.d.ts.map +1 -1
  351. package/dist/hooks/session.js +1500 -223
  352. package/dist/hooks/session.js.map +1 -1
  353. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  354. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  355. package/dist/hud/__tests__/index.test.js +26 -16
  356. package/dist/hud/__tests__/index.test.js.map +1 -1
  357. package/dist/hud/__tests__/reconcile.test.js +83 -78
  358. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  359. package/dist/hud/__tests__/state.test.js +36 -31
  360. package/dist/hud/__tests__/state.test.js.map +1 -1
  361. package/dist/hud/__tests__/tmux.test.js +155 -198
  362. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  363. package/dist/hud/tmux.d.ts +42 -4
  364. package/dist/hud/tmux.d.ts.map +1 -1
  365. package/dist/hud/tmux.js +1017 -139
  366. package/dist/hud/tmux.js.map +1 -1
  367. package/dist/leader/__tests__/contract.test.js +124 -37
  368. package/dist/leader/__tests__/contract.test.js.map +1 -1
  369. package/dist/leader/contract.d.ts +17 -6
  370. package/dist/leader/contract.d.ts.map +1 -1
  371. package/dist/leader/contract.js +102 -26
  372. package/dist/leader/contract.js.map +1 -1
  373. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  374. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  375. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  376. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  377. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  378. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  379. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  380. package/dist/mcp/__tests__/state-server.test.js +1 -1
  381. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  382. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  383. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  384. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  385. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  386. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  387. package/dist/mcp/code-intel-server.js +13 -3
  388. package/dist/mcp/code-intel-server.js.map +1 -1
  389. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  390. package/dist/mcp/hermes-bridge.js +2 -2
  391. package/dist/mcp/hermes-bridge.js.map +1 -1
  392. package/dist/mcp/state-paths.d.ts +78 -4
  393. package/dist/mcp/state-paths.d.ts.map +1 -1
  394. package/dist/mcp/state-paths.js +543 -40
  395. package/dist/mcp/state-paths.js.map +1 -1
  396. package/dist/mcp/state-server.d.ts +4 -4
  397. package/dist/mcp/state-server.d.ts.map +1 -1
  398. package/dist/mcp/wiki-server.d.ts +11 -0
  399. package/dist/mcp/wiki-server.d.ts.map +1 -1
  400. package/dist/mcp/wiki-server.js +9 -2
  401. package/dist/mcp/wiki-server.js.map +1 -1
  402. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  403. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  404. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  405. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  406. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  407. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  408. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  409. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  410. package/dist/modes/base.d.ts +5 -5
  411. package/dist/modes/base.d.ts.map +1 -1
  412. package/dist/modes/base.js +76 -21
  413. package/dist/modes/base.js.map +1 -1
  414. package/dist/native-assets/archive.d.ts +17 -0
  415. package/dist/native-assets/archive.d.ts.map +1 -0
  416. package/dist/native-assets/archive.js +238 -0
  417. package/dist/native-assets/archive.js.map +1 -0
  418. package/dist/native-assets/policy.d.ts +42 -0
  419. package/dist/native-assets/policy.d.ts.map +1 -0
  420. package/dist/native-assets/policy.js +155 -0
  421. package/dist/native-assets/policy.js.map +1 -0
  422. package/dist/notifications/temp-contract.d.ts.map +1 -1
  423. package/dist/notifications/temp-contract.js +4 -0
  424. package/dist/notifications/temp-contract.js.map +1 -1
  425. package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
  426. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  427. package/dist/pipeline/__tests__/stages.test.js +72 -155
  428. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  429. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  430. package/dist/pipeline/orchestrator.js +124 -18
  431. package/dist/pipeline/orchestrator.js.map +1 -1
  432. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  433. package/dist/pipeline/stages/ralplan.js +36 -8
  434. package/dist/pipeline/stages/ralplan.js.map +1 -1
  435. package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
  436. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  437. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  438. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  439. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
  440. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  441. package/dist/ralplan/__tests__/runtime.test.js +85 -101
  442. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  443. package/dist/ralplan/consensus-gate.d.ts +9 -1
  444. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  445. package/dist/ralplan/consensus-gate.js +82 -338
  446. package/dist/ralplan/consensus-gate.js.map +1 -1
  447. package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
  448. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  449. package/dist/ralplan/documented-leader-preflight.js +360 -0
  450. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  451. package/dist/ralplan/runtime.d.ts +2 -1
  452. package/dist/ralplan/runtime.d.ts.map +1 -1
  453. package/dist/ralplan/runtime.js +67 -168
  454. package/dist/ralplan/runtime.js.map +1 -1
  455. package/dist/runtime/__tests__/bridge.test.js +42 -1
  456. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  457. package/dist/runtime/bridge.d.ts +12 -0
  458. package/dist/runtime/bridge.d.ts.map +1 -1
  459. package/dist/runtime/bridge.js +52 -1
  460. package/dist/runtime/bridge.js.map +1 -1
  461. package/dist/runtime/run-state.d.ts +5 -1
  462. package/dist/runtime/run-state.d.ts.map +1 -1
  463. package/dist/runtime/run-state.js +15 -8
  464. package/dist/runtime/run-state.js.map +1 -1
  465. package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
  466. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  467. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  470. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  471. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  472. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  473. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  474. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  475. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  476. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  477. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  478. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  479. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  480. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  481. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  482. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  483. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  484. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  485. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  486. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  487. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  488. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  489. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  490. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  491. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  492. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  493. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  494. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  495. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  496. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  497. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  498. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  499. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  500. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  501. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
  502. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  503. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
  504. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  505. package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
  506. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  507. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  508. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  509. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  510. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  511. package/dist/scripts/codex-native-hook.d.ts +53 -1
  512. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  513. package/dist/scripts/codex-native-hook.js +14448 -1480
  514. package/dist/scripts/codex-native-hook.js.map +1 -1
  515. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  516. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  517. package/dist/scripts/codex-native-pre-post.js +35 -12
  518. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  519. package/dist/scripts/generate-native-release-manifest.js +80 -51
  520. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  521. package/dist/scripts/notify-fallback-watcher.js +78 -115
  522. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  523. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  525. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  526. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  527. package/dist/scripts/notify-hook/state-io.js +5 -0
  528. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  529. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  530. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  531. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  532. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  533. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  534. package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
  535. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  537. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  538. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  539. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +82 -33
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +32 -3
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook.js +79 -82
  552. package/dist/scripts/notify-hook.js.map +1 -1
  553. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  554. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  555. package/dist/scripts/postinstall-advisory.js +49 -0
  556. package/dist/scripts/postinstall-advisory.js.map +1 -0
  557. package/dist/scripts/postinstall.d.ts +1 -1
  558. package/dist/scripts/postinstall.d.ts.map +1 -1
  559. package/dist/scripts/postinstall.js +1 -1
  560. package/dist/scripts/postinstall.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +80 -4
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +3162 -26
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  566. package/dist/scripts/sync-plugin-mirror.js +16 -1
  567. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  568. package/dist/scripts/verify-native-release-assets.js +37 -23
  569. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  570. package/dist/state/__tests__/mode-state-context.test.js +61 -0
  571. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  572. package/dist/state/__tests__/operations.test.js +462 -850
  573. package/dist/state/__tests__/operations.test.js.map +1 -1
  574. package/dist/state/__tests__/skill-active.test.js +400 -2
  575. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  576. package/dist/state/mode-state-context.d.ts +8 -0
  577. package/dist/state/mode-state-context.d.ts.map +1 -1
  578. package/dist/state/mode-state-context.js +73 -0
  579. package/dist/state/mode-state-context.js.map +1 -1
  580. package/dist/state/operations.d.ts +3 -0
  581. package/dist/state/operations.d.ts.map +1 -1
  582. package/dist/state/operations.js +144 -40
  583. package/dist/state/operations.js.map +1 -1
  584. package/dist/state/skill-active.d.ts +18 -1
  585. package/dist/state/skill-active.d.ts.map +1 -1
  586. package/dist/state/skill-active.js +420 -23
  587. package/dist/state/skill-active.js.map +1 -1
  588. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  589. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  590. package/dist/state/workflow-transition-reconcile.js +10 -4
  591. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  592. package/dist/state/workflow-transition.d.ts.map +1 -1
  593. package/dist/state/workflow-transition.js +3 -1
  594. package/dist/state/workflow-transition.js.map +1 -1
  595. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
  596. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  597. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  598. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  599. package/dist/subagents/tracker.d.ts +58 -135
  600. package/dist/subagents/tracker.d.ts.map +1 -1
  601. package/dist/subagents/tracker.js +811 -632
  602. package/dist/subagents/tracker.js.map +1 -1
  603. package/dist/team/__tests__/api-interop.test.js +91 -1
  604. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  605. package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
  606. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  607. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  608. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  609. package/dist/team/__tests__/mcp-comm.test.js +197 -30
  610. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  611. package/dist/team/__tests__/model-contract.test.js +110 -3
  612. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  613. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  614. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  615. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  616. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  617. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  618. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  619. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  620. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  621. package/dist/team/__tests__/phase-controller.test.js +20 -0
  622. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  623. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  624. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  625. package/dist/team/__tests__/runtime.test.js +3963 -428
  626. package/dist/team/__tests__/runtime.test.js.map +1 -1
  627. package/dist/team/__tests__/scaling.test.js +2405 -203
  628. package/dist/team/__tests__/scaling.test.js.map +1 -1
  629. package/dist/team/__tests__/state-root.test.js +117 -2
  630. package/dist/team/__tests__/state-root.test.js.map +1 -1
  631. package/dist/team/__tests__/state.test.js +804 -36
  632. package/dist/team/__tests__/state.test.js.map +1 -1
  633. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  634. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  635. package/dist/team/__tests__/tmux-session.test.js +4099 -1039
  636. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  637. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  638. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  639. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  640. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  641. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  642. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  643. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  644. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  645. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  646. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts.map +1 -1
  652. package/dist/team/api-interop.js +15 -9
  653. package/dist/team/api-interop.js.map +1 -1
  654. package/dist/team/exact-pane.d.ts +23 -0
  655. package/dist/team/exact-pane.d.ts.map +1 -0
  656. package/dist/team/exact-pane.js +133 -0
  657. package/dist/team/exact-pane.js.map +1 -0
  658. package/dist/team/idle-nudge.d.ts +12 -1
  659. package/dist/team/idle-nudge.d.ts.map +1 -1
  660. package/dist/team/idle-nudge.js +41 -14
  661. package/dist/team/idle-nudge.js.map +1 -1
  662. package/dist/team/model-contract.d.ts +2 -5
  663. package/dist/team/model-contract.d.ts.map +1 -1
  664. package/dist/team/model-contract.js +59 -17
  665. package/dist/team/model-contract.js.map +1 -1
  666. package/dist/team/notice-ledger.d.ts +54 -0
  667. package/dist/team/notice-ledger.d.ts.map +1 -0
  668. package/dist/team/notice-ledger.js +259 -0
  669. package/dist/team/notice-ledger.js.map +1 -0
  670. package/dist/team/phase-controller.d.ts.map +1 -1
  671. package/dist/team/phase-controller.js +2 -0
  672. package/dist/team/phase-controller.js.map +1 -1
  673. package/dist/team/runtime.d.ts +4 -1
  674. package/dist/team/runtime.d.ts.map +1 -1
  675. package/dist/team/runtime.js +1527 -291
  676. package/dist/team/runtime.js.map +1 -1
  677. package/dist/team/scaling.d.ts +13 -1
  678. package/dist/team/scaling.d.ts.map +1 -1
  679. package/dist/team/scaling.js +1503 -387
  680. package/dist/team/scaling.js.map +1 -1
  681. package/dist/team/state/dispatch.d.ts.map +1 -1
  682. package/dist/team/state/dispatch.js +26 -8
  683. package/dist/team/state/dispatch.js.map +1 -1
  684. package/dist/team/state/locks.d.ts.map +1 -1
  685. package/dist/team/state/locks.js +61 -13
  686. package/dist/team/state/locks.js.map +1 -1
  687. package/dist/team/state/monitor.d.ts +10 -0
  688. package/dist/team/state/monitor.d.ts.map +1 -1
  689. package/dist/team/state/monitor.js +13 -0
  690. package/dist/team/state/monitor.js.map +1 -1
  691. package/dist/team/state/tasks.d.ts.map +1 -1
  692. package/dist/team/state/tasks.js +4 -3
  693. package/dist/team/state/tasks.js.map +1 -1
  694. package/dist/team/state/types.d.ts +15 -0
  695. package/dist/team/state/types.d.ts.map +1 -1
  696. package/dist/team/state/types.js.map +1 -1
  697. package/dist/team/state-root.d.ts +12 -0
  698. package/dist/team/state-root.d.ts.map +1 -1
  699. package/dist/team/state-root.js +177 -0
  700. package/dist/team/state-root.js.map +1 -1
  701. package/dist/team/state.d.ts +85 -1
  702. package/dist/team/state.d.ts.map +1 -1
  703. package/dist/team/state.js +672 -129
  704. package/dist/team/state.js.map +1 -1
  705. package/dist/team/team-ops.d.ts +5 -0
  706. package/dist/team/team-ops.d.ts.map +1 -1
  707. package/dist/team/team-ops.js +4 -0
  708. package/dist/team/team-ops.js.map +1 -1
  709. package/dist/team/tmux-session.d.ts +163 -27
  710. package/dist/team/tmux-session.d.ts.map +1 -1
  711. package/dist/team/tmux-session.js +1682 -376
  712. package/dist/team/tmux-session.js.map +1 -1
  713. package/dist/team/worktree.d.ts.map +1 -1
  714. package/dist/team/worktree.js +4 -0
  715. package/dist/team/worktree.js.map +1 -1
  716. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  717. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  718. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts +35 -1
  721. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  722. package/dist/ultragoal/artifacts.js +232 -46
  723. package/dist/ultragoal/artifacts.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +114 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/__tests__/paths.test.js +21 -1
  729. package/dist/utils/__tests__/paths.test.js.map +1 -1
  730. package/dist/utils/file-durability.d.ts +25 -0
  731. package/dist/utils/file-durability.d.ts.map +1 -0
  732. package/dist/utils/file-durability.js +67 -0
  733. package/dist/utils/file-durability.js.map +1 -0
  734. package/dist/utils/paths.d.ts.map +1 -1
  735. package/dist/utils/paths.js +14 -6
  736. package/dist/utils/paths.js.map +1 -1
  737. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  738. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  742. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  743. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  744. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  745. package/dist/wiki/lint.d.ts +4 -1
  746. package/dist/wiki/lint.d.ts.map +1 -1
  747. package/dist/wiki/lint.js +3 -2
  748. package/dist/wiki/lint.js.map +1 -1
  749. package/package.json +13 -5
  750. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  751. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
  752. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  753. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  754. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  755. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
  756. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  757. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  758. package/skills/autopilot/SKILL.md +13 -5
  759. package/skills/cancel/SKILL.md +30 -85
  760. package/skills/ralph/SKILL.md +6 -4
  761. package/skills/ralplan/SKILL.md +16 -17
  762. package/skills/team/SKILL.md +13 -9
  763. package/skills/ultragoal/SKILL.md +3 -2
  764. package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
  765. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  766. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  767. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  768. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  769. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  770. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  772. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  773. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  774. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  775. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
  776. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
  777. package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
  778. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  779. package/src/scripts/codex-native-hook.ts +14238 -1429
  780. package/src/scripts/codex-native-pre-post.ts +43 -12
  781. package/src/scripts/generate-native-release-manifest.ts +83 -79
  782. package/src/scripts/notify-fallback-watcher.ts +91 -122
  783. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  784. package/src/scripts/notify-hook/state-io.ts +11 -0
  785. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  786. package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
  787. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  788. package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
  789. package/src/scripts/notify-hook/team-worker.ts +82 -31
  790. package/src/scripts/notify-hook/tmux-injection.ts +33 -3
  791. package/src/scripts/notify-hook.ts +84 -82
  792. package/src/scripts/postinstall-advisory.ts +67 -0
  793. package/src/scripts/postinstall.ts +1 -1
  794. package/src/scripts/smoke-packed-install.ts +3608 -39
  795. package/src/scripts/sync-plugin-mirror.ts +17 -1
  796. package/src/scripts/verify-native-release-assets.ts +39 -31
  797. package/templates/AGENTS.md +1 -0
  798. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  799. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  800. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  801. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  802. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  803. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  804. package/dist/subagents/adapted-role-binding.js +0 -75
  805. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -1,27 +1,61 @@
1
1
  import { randomUUID } from 'node:crypto';
2
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';
3
+ import { readFile } from 'node:fs/promises';
4
4
  import { hostname } from 'node:os';
5
5
  import { basename, dirname, join } from 'node:path';
6
6
  import { AGENT_DEFINITIONS } from '../agents/definitions.js';
7
- import { getBaseStateDir, getBaseStateDirWithSource } from '../state/paths.js';
8
- import { canonicalizeOriginCwd } from '../leader/contract.js';
7
+ import { getBaseStateDir } from '../state/paths.js';
9
8
  import { codexAgentsDir, projectCodexAgentsDir } from '../utils/paths.js';
10
9
  export const SUBAGENT_TRACKING_SCHEMA_VERSION = 1;
11
10
  export const DEFAULT_SUBAGENT_ACTIVE_WINDOW_MS = 120_000;
12
- export const OMX_ADAPTED_PROVENANCE = 'omx_adapted';
13
11
  export const NATIVE_SUBAGENT_PROVENANCE = 'native_subagent';
12
+ // Legacy descriptive provenance written by pre-authority releases. Tolerated as
13
+ // descriptive-only evidence: it never grants reopen authority and never blocks a
14
+ // newly attested valid direct child, but it is preserved so authority-consistency
15
+ // checks can distinguish recognized legacy data from unknown descriptive views.
16
+ export const DESCRIPTIVE_ADAPTED_PROVENANCE = 'adapted';
14
17
  const KNOWN_TYPED_AGENT_ROLES = new Set(Object.keys(AGENT_DEFINITIONS).map((role) => role.toLowerCase()));
15
18
  export function subagentTrackingPath(cwd) {
16
19
  return join(getBaseStateDir(cwd), 'subagent-tracking.json');
17
20
  }
18
- export function resolveInstalledRoleName(role, codexHomeOverride) {
21
+ /**
22
+ * Authority-relevant projection of a tracker state. The generic descriptive
23
+ * writer is allowed to publish descriptive churn, but it must never roll back
24
+ * an authority decision that another process committed after the caller read
25
+ * its snapshot. Comparing this projection is the durable equivalent of a CAS
26
+ * on the authority surface without forcing every descriptive caller to hold
27
+ * the tracker lock across its own read/modify/write.
28
+ */
29
+ function authorityRevisionOf(state) {
30
+ const sessions = Object.keys(state.sessions).sort().map((sessionId) => {
31
+ const session = state.sessions[sessionId];
32
+ const threads = Object.keys(session.threads).sort().map((threadId) => {
33
+ const thread = session.threads[threadId];
34
+ return [
35
+ threadId,
36
+ thread.kind,
37
+ thread.provenance_kind ?? '',
38
+ thread.direct_child_root_id ?? '',
39
+ thread.direct_child_parent_id ?? '',
40
+ thread.reopen_authority_revoked === true ? '1' : '',
41
+ thread.reopen_authority_conflict_reason ?? '',
42
+ thread.reopen_authority_conflict_at ?? '',
43
+ // Availability gates whether an eligible child is actually emitted as
44
+ // a reopen target, so it is part of the authority surface.
45
+ thread.status ?? '',
46
+ ].join('\u0001');
47
+ });
48
+ return [sessionId, session.leader_thread_id ?? '', ...threads].join('\u0002');
49
+ });
50
+ return sessions.join('\u0003');
51
+ }
52
+ export function resolveInstalledRoleName(role, codexHomeOverride, projectRootOverride) {
19
53
  const normalizedRole = role.trim().toLowerCase();
20
54
  if (!normalizedRole)
21
55
  return null;
22
56
  if (KNOWN_TYPED_AGENT_ROLES.has(normalizedRole))
23
57
  return normalizedRole;
24
- for (const agentsDir of [codexAgentsDir(codexHomeOverride), projectCodexAgentsDir()]) {
58
+ for (const agentsDir of [codexAgentsDir(codexHomeOverride), projectCodexAgentsDir(projectRootOverride)]) {
25
59
  try {
26
60
  for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
27
61
  if (!entry.isFile() || !entry.name.endsWith('.toml'))
@@ -41,7 +75,6 @@ export function createSubagentTrackingState() {
41
75
  return {
42
76
  schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
43
77
  sessions: {},
44
- pending_role_intents: [],
45
78
  };
46
79
  }
47
80
  function normalizeSubagentStatus(value) {
@@ -119,40 +152,6 @@ export function isTrustedSubagentThread(session, threadId) {
119
152
  return false;
120
153
  return session.threads[normalizedThreadId]?.kind === 'subagent';
121
154
  }
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
- }
156
155
  export function normalizeSubagentTrackingState(input) {
157
156
  const base = createSubagentTrackingState();
158
157
  if (!input || typeof input !== 'object')
@@ -190,9 +189,11 @@ export function normalizeSubagentTrackingState(input) {
190
189
  turn_count: typeof candidate.turn_count === 'number' && Number.isFinite(candidate.turn_count) && candidate.turn_count > 0 ? candidate.turn_count : 1,
191
190
  ...(typeof candidate.mode === 'string' && candidate.mode.trim().length > 0 ? { mode: candidate.mode } : {}),
192
191
  ...(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
- : {}),
192
+ ...(candidate.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
193
+ ? { provenance_kind: NATIVE_SUBAGENT_PROVENANCE }
194
+ : candidate.provenance_kind === DESCRIPTIVE_ADAPTED_PROVENANCE
195
+ ? { provenance_kind: DESCRIPTIVE_ADAPTED_PROVENANCE }
196
+ : {}),
196
197
  ...(typeof candidate.lane_id === 'string' && candidate.lane_id.trim().length > 0 ? { lane_id: candidate.lane_id.trim() } : {}),
197
198
  ...(typeof candidate.scope === 'string' && candidate.scope.trim().length > 0 ? { scope: candidate.scope.trim() } : {}),
198
199
  ...(typeof candidate.agent_nickname === 'string' && candidate.agent_nickname.trim().length > 0
@@ -217,37 +218,117 @@ export function normalizeSubagentTrackingState(input) {
217
218
  ...(typeof candidate.resume_failure_reason === 'string' && candidate.resume_failure_reason.trim().length > 0
218
219
  ? { resume_failure_reason: candidate.resume_failure_reason.trim() }
219
220
  : {}),
221
+ ...(typeof candidate.direct_child_root_id === 'string' && candidate.direct_child_root_id.trim().length > 0
222
+ ? { direct_child_root_id: candidate.direct_child_root_id.trim() }
223
+ : {}),
224
+ ...(typeof candidate.direct_child_parent_id === 'string' && candidate.direct_child_parent_id.trim().length > 0
225
+ ? { direct_child_parent_id: candidate.direct_child_parent_id.trim() }
226
+ : {}),
227
+ ...(candidate.reopen_authority_revoked === true ? { reopen_authority_revoked: true } : {}),
228
+ ...(typeof candidate.reopen_authority_conflict_reason === 'string' && candidate.reopen_authority_conflict_reason.trim()
229
+ ? { reopen_authority_conflict_reason: candidate.reopen_authority_conflict_reason.trim() }
230
+ : {}),
231
+ ...(typeof candidate.reopen_authority_conflict_at === 'string' && candidate.reopen_authority_conflict_at.trim()
232
+ ? { reopen_authority_conflict_at: candidate.reopen_authority_conflict_at.trim() }
233
+ : {}),
220
234
  };
221
235
  }
222
236
  const sessionCandidate = rawSession;
223
237
  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()
229
- : undefined;
230
238
  const updatedAt = typeof sessionCandidate.updated_at === 'string' && sessionCandidate.updated_at.trim().length > 0
231
239
  ? sessionCandidate.updated_at
232
240
  : new Date(0).toISOString();
233
241
  sessions[sessionId] = {
234
242
  session_id: sessionId,
235
- leader_thread_id: leaderThreadId,
236
- ...(leaderAttestedAt ? { leader_attested_at: leaderAttestedAt } : {}),
237
- ...(leaderAttestSource ? { leader_attest_source: leaderAttestSource } : {}),
243
+ ...(leaderThreadId ? { leader_thread_id: leaderThreadId } : {}),
238
244
  updated_at: updatedAt,
239
245
  threads,
240
246
  };
241
247
  }
242
- const pendingRoleIntents = Array.isArray(parsed.pending_role_intents)
243
- ? parsed.pending_role_intents.map((intent) => normalizePendingRoleIntent(intent)).filter((intent) => intent !== null)
244
- : [];
245
248
  return {
246
249
  schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
247
250
  sessions,
248
- pending_role_intents: pendingRoleIntents,
249
251
  };
250
252
  }
253
+ function parseStrictSubagentTrackingState(raw) {
254
+ let parsed;
255
+ try {
256
+ parsed = JSON.parse(raw);
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ if (!parsed || typeof parsed !== 'object')
262
+ return null;
263
+ const candidate = parsed;
264
+ if (candidate.schemaVersion !== SUBAGENT_TRACKING_SCHEMA_VERSION || !candidate.sessions || typeof candidate.sessions !== 'object' || Array.isArray(candidate.sessions))
265
+ return null;
266
+ const normalizedSessionIds = new Set();
267
+ for (const [sessionId, sessionValue] of Object.entries(candidate.sessions)) {
268
+ if (!sessionId || sessionId.trim() !== sessionId || normalizedSessionIds.has(sessionId.trim()))
269
+ return null;
270
+ normalizedSessionIds.add(sessionId.trim());
271
+ if (!sessionValue || typeof sessionValue !== 'object' || Array.isArray(sessionValue))
272
+ return null;
273
+ const session = sessionValue;
274
+ if (session.session_id !== sessionId || typeof session.updated_at !== 'string' || !session.updated_at.trim())
275
+ return null;
276
+ if (!session.threads || typeof session.threads !== 'object' || Array.isArray(session.threads))
277
+ return null;
278
+ if (session.leader_thread_id !== undefined && (typeof session.leader_thread_id !== 'string' || !session.leader_thread_id.trim() || session.leader_thread_id.trim() !== session.leader_thread_id))
279
+ return null;
280
+ const normalizedThreadIds = new Set();
281
+ for (const [threadId, threadValue] of Object.entries(session.threads)) {
282
+ if (!threadId || threadId.trim() !== threadId || normalizedThreadIds.has(threadId.trim()))
283
+ return null;
284
+ normalizedThreadIds.add(threadId.trim());
285
+ if (!threadValue || typeof threadValue !== 'object' || Array.isArray(threadValue))
286
+ return null;
287
+ const thread = threadValue;
288
+ if (thread.thread_id !== threadId || (thread.kind !== 'leader' && thread.kind !== 'subagent'))
289
+ return null;
290
+ if (typeof thread.first_seen_at !== 'string' || !thread.first_seen_at.trim())
291
+ return null;
292
+ if (typeof thread.last_seen_at !== 'string' || !thread.last_seen_at.trim())
293
+ return null;
294
+ if (typeof thread.turn_count !== 'number' || !Number.isFinite(thread.turn_count) || thread.turn_count < 1)
295
+ return null;
296
+ const hasAuthorityEvidence = thread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
297
+ || thread.direct_child_root_id !== undefined
298
+ || thread.direct_child_parent_id !== undefined
299
+ || thread.reopen_authority_revoked !== undefined
300
+ || thread.reopen_authority_conflict_reason !== undefined
301
+ || thread.reopen_authority_conflict_at !== undefined;
302
+ if (hasAuthorityEvidence && thread.status !== undefined && normalizeSubagentStatus(thread.status) === undefined)
303
+ return null;
304
+ if (hasAuthorityEvidence && thread.provenance_kind !== undefined && thread.provenance_kind !== NATIVE_SUBAGENT_PROVENANCE)
305
+ return null;
306
+ for (const field of ['direct_child_root_id', 'direct_child_parent_id', 'reopen_authority_conflict_reason', 'reopen_authority_conflict_at']) {
307
+ if (thread[field] !== undefined && (typeof thread[field] !== 'string' || !thread[field].trim() || thread[field].trim() !== thread[field]))
308
+ return null;
309
+ }
310
+ if (thread.reopen_authority_revoked !== undefined && typeof thread.reopen_authority_revoked !== 'boolean')
311
+ return null;
312
+ if ((thread.direct_child_root_id === undefined) !== (thread.direct_child_parent_id === undefined))
313
+ return null;
314
+ if ((thread.reopen_authority_conflict_reason !== undefined || thread.reopen_authority_conflict_at !== undefined) && thread.reopen_authority_revoked !== true)
315
+ return null;
316
+ }
317
+ }
318
+ return normalizeSubagentTrackingState(parsed);
319
+ }
320
+ function readSubagentTrackingStateStrictSync(cwd) {
321
+ const path = subagentTrackingPath(cwd);
322
+ if (!existsSync(path))
323
+ return { ok: true, state: createSubagentTrackingState() };
324
+ try {
325
+ const state = parseStrictSubagentTrackingState(readFileSync(path, 'utf-8'));
326
+ return state ? { ok: true, state } : { ok: false };
327
+ }
328
+ catch {
329
+ return { ok: false };
330
+ }
331
+ }
251
332
  function atomicTrackingTempPath(path) {
252
333
  return `${path}.${process.pid}.${randomUUID()}.tmp`;
253
334
  }
@@ -587,41 +668,14 @@ export function withCrossProcessFileLockSync(resourcePath, operation, options =
587
668
  releaseCrossProcessFileLock(lockPath, token);
588
669
  }
589
670
  }
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
- }
671
+ function threadIsTrackedAsSubagent(state, threadId) {
672
+ const id = threadId.trim();
673
+ return Boolean(id) && Object.values(state.sessions).some((session) => isTrustedSubagentThread(session, id));
600
674
  }
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
- }
675
+ export function hasLeaderSubagentCollision(state, leaderThreadId) {
676
+ return threadIsTrackedAsSubagent(state, leaderThreadId);
624
677
  }
678
+ // Strict reads preserve fail-closed behavior for security-sensitive tracker decisions.
625
679
  export async function readSubagentTrackingStateStrict(cwd) {
626
680
  const path = subagentTrackingPath(cwd);
627
681
  let raw;
@@ -633,22 +687,8 @@ export async function readSubagentTrackingStateStrict(cwd) {
633
687
  return { ok: true, state: createSubagentTrackingState() };
634
688
  return { ok: false };
635
689
  }
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;
690
+ const state = parseStrictSubagentTrackingState(raw);
691
+ return state ? { ok: true, state } : { ok: false };
652
692
  }
653
693
  function writeSubagentTrackingStateSync(cwd, state, publish) {
654
694
  const normalized = normalizeSubagentTrackingState(state);
@@ -676,526 +716,61 @@ export async function readSubagentTrackingState(cwd) {
676
716
  }
677
717
  }
678
718
  export async function writeSubagentTrackingState(cwd, state) {
679
- const normalized = normalizeSubagentTrackingState(state);
680
719
  const path = subagentTrackingPath(cwd);
681
- await mkdir(dirname(path), { recursive: true });
682
- const temporaryPath = atomicTrackingTempPath(path);
683
- await writeFile(temporaryPath, `${JSON.stringify(normalized, null, 2)}\n`);
684
- await rename(temporaryPath, path);
685
- return path;
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);
720
+ // Fail closed on every side of the write. The proposed state must survive
721
+ // strict authority validation (tolerant inputs must not launder malformed
722
+ // authority into a valid-looking attestation); existing on-disk bytes must
723
+ // not be overwritten when they fail strict parsing; and the publication must
724
+ // not roll back an authority decision committed after the caller read its
725
+ // snapshot. The whole read/validate/compare/publish transaction is
726
+ // serialized under the tracker lock.
727
+ const proposedRaw = JSON.stringify(state);
728
+ if (!parseStrictSubagentTrackingState(proposedRaw))
729
+ throw new Error('Malformed subagent tracker authority state');
730
+ const normalized = normalizeSubagentTrackingState(state);
731
+ const contents = `${JSON.stringify(normalized, null, 2)}\n`;
732
+ const proposedAuthorityRevision = authorityRevisionOf(normalized);
733
+ return withCrossProcessFileLockSync(path, (lock) => {
734
+ if (existsSync(path)) {
735
+ let raw;
736
+ try {
737
+ raw = readFileSync(path, 'utf-8');
1054
738
  }
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);
739
+ catch {
740
+ throw new Error('Malformed subagent tracker authority state');
1072
741
  }
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,
742
+ const current = parseStrictSubagentTrackingState(raw);
743
+ if (!current)
744
+ throw new Error('Malformed subagent tracker authority state');
745
+ // The generic writer is descriptive-only: it may publish descriptive
746
+ // churn, but it must never CHANGE the authority surface in any
747
+ // direction. Granting, revoking, or rolling back authority is reserved
748
+ // for the lock-owning authority transactions. Requiring the proposed
749
+ // authority projection to equal the current on-disk projection makes a
750
+ // stale snapshot, a fresh-read escalation, and a concurrent interleaving
751
+ // all fail closed with one rule.
752
+ if (authorityRevisionOf(current) !== proposedAuthorityRevision) {
753
+ throw new Error('Stale subagent tracker authority state');
1092
754
  }
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
755
  }
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';
756
+ else if (proposedAuthorityRevision !== authorityRevisionOf(createSubagentTrackingState())) {
757
+ // Creating the tracker from nothing must not mint reopen authority.
758
+ // `leader_thread_id` is deliberately NOT included: it is ordinary
759
+ // descriptive lifecycle data that legitimate first writers set, and it
760
+ // never by itself makes a thread an eligible reopen target.
761
+ const mintsAuthority = Object.values(normalized.sessions).some((session) => Object.values(session.threads).some((thread) => thread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
762
+ || thread.direct_child_root_id !== undefined
763
+ || thread.direct_child_parent_id !== undefined
764
+ || thread.reopen_authority_revoked !== undefined
765
+ || thread.reopen_authority_conflict_reason !== undefined
766
+ || thread.reopen_authority_conflict_at !== undefined));
767
+ if (mintsAuthority)
768
+ throw new Error('Stale subagent tracker authority state');
1164
769
  }
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] : [];
770
+ mkdirSync(dirname(path), { recursive: true });
771
+ lock.assertOwnership();
772
+ lock.publish(contents);
773
+ return path;
1199
774
  });
1200
775
  }
1201
776
  export function recordSubagentTurn(state, input) {
@@ -1263,11 +838,13 @@ export function recordSubagentTurn(state, input) {
1263
838
  : preservedCompletion),
1264
839
  ...(input.mode?.trim() ? { mode: input.mode.trim() } : existingThread?.mode ? { mode: existingThread.mode } : {}),
1265
840
  ...(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
- : {}),
841
+ ...(input.provenanceKind === NATIVE_SUBAGENT_PROVENANCE
842
+ ? { provenance_kind: NATIVE_SUBAGENT_PROVENANCE }
843
+ : existingThread?.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
844
+ ? { provenance_kind: NATIVE_SUBAGENT_PROVENANCE }
845
+ : existingThread?.provenance_kind === DESCRIPTIVE_ADAPTED_PROVENANCE
846
+ ? { provenance_kind: DESCRIPTIVE_ADAPTED_PROVENANCE }
847
+ : {}),
1271
848
  ...(input.laneId?.trim() ? { lane_id: input.laneId.trim() } : existingThread?.lane_id ? { lane_id: existingThread.lane_id } : {}),
1272
849
  ...(input.scope?.trim() ? { scope: input.scope.trim() } : existingThread?.scope ? { scope: existingThread.scope } : {}),
1273
850
  ...(input.agentNickname?.trim()
@@ -1301,6 +878,11 @@ export function recordSubagentTurn(state, input) {
1301
878
  : existingThread?.resume_failure_reason
1302
879
  ? { resume_failure_reason: existingThread.resume_failure_reason }
1303
880
  : {}),
881
+ ...(existingThread?.direct_child_root_id ? { direct_child_root_id: existingThread.direct_child_root_id } : {}),
882
+ ...(existingThread?.direct_child_parent_id ? { direct_child_parent_id: existingThread.direct_child_parent_id } : {}),
883
+ ...(existingThread?.reopen_authority_revoked ? { reopen_authority_revoked: true } : {}),
884
+ ...(existingThread?.reopen_authority_conflict_reason ? { reopen_authority_conflict_reason: existingThread.reopen_authority_conflict_reason } : {}),
885
+ ...(existingThread?.reopen_authority_conflict_at ? { reopen_authority_conflict_at: existingThread.reopen_authority_conflict_at } : {}),
1304
886
  };
1305
887
  const threads = {
1306
888
  ...existingSession.threads,
@@ -1315,11 +897,6 @@ export function recordSubagentTurn(state, input) {
1315
897
  normalized.sessions[sessionId] = {
1316
898
  session_id: sessionId,
1317
899
  ...(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 } : {}),
1323
900
  updated_at: timestamp,
1324
901
  threads,
1325
902
  };
@@ -1327,13 +904,615 @@ export function recordSubagentTurn(state, input) {
1327
904
  }
1328
905
  export async function recordSubagentTurnForSession(cwd, input) {
1329
906
  return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
1330
- const current = readSubagentTrackingStateSync(cwd);
1331
- const next = recordSubagentTurn(current, input);
907
+ const strict = readSubagentTrackingStateStrictSync(cwd);
908
+ if (!strict.ok)
909
+ throw new Error('Malformed subagent tracker authority state');
910
+ const next = recordSubagentTurn(strict.state, input);
911
+ context.assertOwnership();
912
+ writeSubagentTrackingStateSync(cwd, next, context.publish);
913
+ return next;
914
+ });
915
+ }
916
+ export function recordNativeSubagentAuthorityObservation(cwd, observation) {
917
+ const requestedSessionIds = [...new Set(observation.sessionIds.map((value) => value.trim()).filter(Boolean))];
918
+ const childThreadId = observation.childThreadId.trim();
919
+ const parentThreadId = observation.parentThreadId.trim();
920
+ const rootNativeSessionId = observation.rootNativeSessionId?.trim() ?? '';
921
+ const implicatedChildThreadIds = [...new Set([
922
+ childThreadId,
923
+ ...(observation.implicatedChildThreadIds ?? []).map((value) => value.trim()),
924
+ ].filter(Boolean))];
925
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
926
+ const strict = readSubagentTrackingStateStrictSync(cwd);
927
+ if (!strict.ok)
928
+ throw new Error('Malformed subagent tracker authority state');
929
+ let next = strict.state;
930
+ const existingSessionIds = Object.entries(next.sessions)
931
+ .filter(([, session]) => implicatedChildThreadIds.some((id) => Boolean(session.threads[id]?.direct_child_root_id || session.threads[id]?.reopen_authority_revoked)))
932
+ .map(([sessionId]) => sessionId);
933
+ const sessionIds = [...new Set([...requestedSessionIds, ...existingSessionIds])];
934
+ const existingBindings = Object.values(next.sessions)
935
+ .map((session) => session.threads[childThreadId])
936
+ .filter((thread) => Boolean(thread?.direct_child_root_id));
937
+ const authorityEvidence = observation.authorityEvidence ?? (rootNativeSessionId ? 'valid' : 'absent');
938
+ const globalConflict = authorityEvidence === 'untrusted' && existingBindings.length > 0
939
+ || authorityEvidence === 'valid' && existingBindings.some((thread) => thread.direct_child_root_id !== rootNativeSessionId || thread.direct_child_parent_id !== parentThreadId);
940
+ const timestamp = observation.timestamp ?? new Date().toISOString();
941
+ if (authorityEvidence === 'untrusted') {
942
+ for (const session of Object.values(next.sessions)) {
943
+ for (const implicatedId of implicatedChildThreadIds) {
944
+ const implicatedThread = session.threads[implicatedId];
945
+ if (!implicatedThread?.direct_child_root_id && !implicatedThread?.reopen_authority_revoked)
946
+ continue;
947
+ implicatedThread.reopen_authority_revoked = true;
948
+ implicatedThread.reopen_authority_conflict_reason = 'identity_untrusted';
949
+ implicatedThread.reopen_authority_conflict_at = timestamp;
950
+ }
951
+ }
952
+ }
953
+ for (const sessionId of sessionIds) {
954
+ if (parentThreadId && parentThreadId !== childThreadId) {
955
+ next = recordSubagentTurn(next, { sessionId, threadId: parentThreadId, kind: 'leader', timestamp });
956
+ }
957
+ next = recordSubagentTurn(next, {
958
+ sessionId,
959
+ threadId: childThreadId,
960
+ kind: 'subagent',
961
+ ...(parentThreadId && parentThreadId !== childThreadId ? { leaderThreadId: parentThreadId } : {}),
962
+ mode: observation.mode,
963
+ timestamp,
964
+ });
965
+ const thread = next.sessions[sessionId]?.threads[childThreadId];
966
+ if (!thread)
967
+ continue;
968
+ const mayAttest = authorityEvidence === 'valid'
969
+ && Boolean(rootNativeSessionId)
970
+ && childThreadId !== rootNativeSessionId
971
+ && parentThreadId === rootNativeSessionId;
972
+ if (globalConflict) {
973
+ thread.reopen_authority_revoked = true;
974
+ thread.reopen_authority_conflict_reason = 'root_or_parent_mismatch';
975
+ thread.reopen_authority_conflict_at = timestamp;
976
+ }
977
+ else if (mayAttest) {
978
+ thread.direct_child_root_id = rootNativeSessionId;
979
+ thread.direct_child_parent_id = parentThreadId;
980
+ thread.provenance_kind = NATIVE_SUBAGENT_PROVENANCE;
981
+ delete thread.reopen_authority_revoked;
982
+ delete thread.reopen_authority_conflict_reason;
983
+ delete thread.reopen_authority_conflict_at;
984
+ }
985
+ }
1332
986
  context.assertOwnership();
1333
987
  writeSubagentTrackingStateSync(cwd, next, context.publish);
1334
988
  return next;
1335
989
  });
1336
990
  }
991
+ /**
992
+ * Durable fence for authority revocations that could not be published.
993
+ *
994
+ * A negative identity observation is only meaningful if reopen consumption can
995
+ * see it. When the revocation transaction fails (lock exhaustion, malformed
996
+ * bytes, I/O error) the affected ids are recorded here instead of being
997
+ * silently dropped, and `consumeDirectChildReopenContext` treats any fenced id
998
+ * as denied until the revocation is durably applied.
999
+ */
1000
+ function authorityFencePath(cwd) {
1001
+ return `${subagentTrackingPath(cwd)}.authority-fence.json`;
1002
+ }
1003
+ /** Read the fenced ids. An unreadable/malformed fence fails closed for all ids. */
1004
+ function readAuthorityFence(cwd) {
1005
+ const path = authorityFencePath(cwd);
1006
+ if (!existsSync(path))
1007
+ return { ok: true, ids: new Set() };
1008
+ try {
1009
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
1010
+ if (!Array.isArray(parsed.ids))
1011
+ return { ok: false, ids: new Set() };
1012
+ const ids = parsed.ids.filter((id) => typeof id === 'string' && Boolean(id.trim()));
1013
+ if (ids.length !== parsed.ids.length)
1014
+ return { ok: false, ids: new Set() };
1015
+ return { ok: true, ids: new Set(ids.map((id) => id.trim())) };
1016
+ }
1017
+ catch {
1018
+ return { ok: false, ids: new Set() };
1019
+ }
1020
+ }
1021
+ /** Upper bound on fenced ids, so repeated failures cannot grow the fence without limit. */
1022
+ export const AUTHORITY_FENCE_MAX_IDS = 512;
1023
+ /**
1024
+ * Record ids whose revocation could not be persisted, so reopen stays denied.
1025
+ *
1026
+ * Every fence mutation runs under a dedicated cross-process lock on the fence
1027
+ * path so read/merge/publish is atomic with respect to concurrent fencing and
1028
+ * clearing. Returns true when the denial is durable; the caller must treat
1029
+ * false as an unresolved fail-closed condition.
1030
+ */
1031
+ export function fenceNativeSubagentAuthorities(cwd, childThreadIds, reason) {
1032
+ const ids = childThreadIds.map((value) => value.trim()).filter(Boolean);
1033
+ if (!ids.length)
1034
+ return true;
1035
+ const path = authorityFencePath(cwd);
1036
+ try {
1037
+ return withCrossProcessFileLockSync(path, (lock) => {
1038
+ const existing = readAuthorityFence(cwd);
1039
+ // An unreadable fence already denies every id; do not weaken it by
1040
+ // rewriting it into a narrower explicit id list.
1041
+ if (!existing.ok)
1042
+ return true;
1043
+ const merged = [...new Set([...existing.ids, ...ids])].sort();
1044
+ if (merged.length > AUTHORITY_FENCE_MAX_IDS) {
1045
+ // Refuse to grow past the cap by writing a deliberately unreadable
1046
+ // sentinel: a global denial is strictly stronger than a partial list.
1047
+ mkdirSync(dirname(path), { recursive: true });
1048
+ lock.assertOwnership();
1049
+ lock.publish(`${JSON.stringify({ ids: 'all', reason: 'fence_capacity_exceeded', fenced_at: new Date().toISOString() }, null, 2)}\n`);
1050
+ return true;
1051
+ }
1052
+ mkdirSync(dirname(path), { recursive: true });
1053
+ lock.assertOwnership();
1054
+ lock.publish(`${JSON.stringify({ ids: merged, reason, fenced_at: new Date().toISOString() }, null, 2)}\n`);
1055
+ return true;
1056
+ });
1057
+ }
1058
+ catch {
1059
+ // The denial could not be made durable. The caller must surface this
1060
+ // rather than continue as if the revocation had been applied.
1061
+ return false;
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Last-resort global denial used when BOTH the tracker revocation transaction
1066
+ * and the locked fence publication fail. It deliberately avoids the fence lock
1067
+ * (which may be exactly what is unavailable) and writes an intentionally
1068
+ * unreadable sentinel directly, because `readAuthorityFence` treats any
1069
+ * unreadable fence as a denial of every id. Returns true when the denial is
1070
+ * durable on disk.
1071
+ */
1072
+ export function forceGlobalAuthorityDenial(cwd, reason) {
1073
+ const path = authorityFencePath(cwd);
1074
+ const contents = `${JSON.stringify({ ids: 'all', reason, fenced_at: new Date().toISOString() }, null, 2)}\n`;
1075
+ try {
1076
+ mkdirSync(dirname(path), { recursive: true });
1077
+ const temporaryPath = atomicTrackingTempPath(path);
1078
+ writeFileSync(temporaryPath, contents);
1079
+ renameSync(temporaryPath, path);
1080
+ return true;
1081
+ }
1082
+ catch {
1083
+ try {
1084
+ // Even a partial/truncated write is safe here: unreadable means denied.
1085
+ writeFileSync(path, contents);
1086
+ return true;
1087
+ }
1088
+ catch {
1089
+ return false;
1090
+ }
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Drop ids from the fence once their revocation is durably published.
1095
+ *
1096
+ * Runs under the fence lock and re-reads inside it, so it can never clear
1097
+ * based on a stale snapshot. An unreadable fence is a global denial and is
1098
+ * NEVER destroyed here: only an explicit, fully reconciled id list is removed.
1099
+ */
1100
+ function clearAuthorityFence(cwd, childThreadIds) {
1101
+ const path = authorityFencePath(cwd);
1102
+ if (!existsSync(path))
1103
+ return;
1104
+ const cleared = new Set(childThreadIds.map((value) => value.trim()).filter(Boolean));
1105
+ if (!cleared.size)
1106
+ return;
1107
+ try {
1108
+ withCrossProcessFileLockSync(path, (lock) => {
1109
+ const existing = readAuthorityFence(cwd);
1110
+ // Preserve an unreadable/global denial. Reconciling it requires explicit
1111
+ // operator action, not an incidental unrelated revocation.
1112
+ if (!existing.ok)
1113
+ return;
1114
+ const remaining = [...existing.ids].filter((id) => !cleared.has(id)).sort();
1115
+ if (remaining.length === existing.ids.size)
1116
+ return;
1117
+ lock.assertOwnership();
1118
+ if (!remaining.length) {
1119
+ unlinkSync(path);
1120
+ return;
1121
+ }
1122
+ lock.publish(`${JSON.stringify({ ids: remaining, reason: 'partial_clear', fenced_at: new Date().toISOString() }, null, 2)}\n`);
1123
+ });
1124
+ }
1125
+ catch {
1126
+ // Leaving a stale fence entry is fail-closed and therefore acceptable.
1127
+ }
1128
+ }
1129
+ export function revokeNativeSubagentAuthorities(cwd, childThreadIds, reason = 'identity_untrusted') {
1130
+ const ids = [...new Set(childThreadIds.map((value) => value.trim()).filter(Boolean))];
1131
+ return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
1132
+ const strict = readSubagentTrackingStateStrictSync(cwd);
1133
+ if (!strict.ok)
1134
+ throw new Error('Malformed subagent tracker authority state');
1135
+ const next = strict.state;
1136
+ const timestamp = new Date().toISOString();
1137
+ let changed = false;
1138
+ for (const session of Object.values(next.sessions)) {
1139
+ for (const id of ids) {
1140
+ const thread = session.threads[id];
1141
+ if (!thread?.direct_child_root_id && !thread?.reopen_authority_revoked)
1142
+ continue;
1143
+ thread.reopen_authority_revoked = true;
1144
+ thread.reopen_authority_conflict_reason = reason;
1145
+ thread.reopen_authority_conflict_at = timestamp;
1146
+ changed = true;
1147
+ }
1148
+ }
1149
+ if (changed) {
1150
+ context.assertOwnership();
1151
+ writeSubagentTrackingStateSync(cwd, next, context.publish);
1152
+ }
1153
+ // The revocation is now durable for these ids (either just published, or
1154
+ // already present on disk), so any prior fence for them can be released.
1155
+ clearAuthorityFence(cwd, ids);
1156
+ return next;
1157
+ });
1158
+ }
1159
+ function formatReopenMetadata(entry) {
1160
+ const values = [
1161
+ entry.role ? `role: ${entry.role}` : null,
1162
+ entry.laneId ? `lane: ${entry.laneId}` : null,
1163
+ entry.scope ? `scope: ${entry.scope}` : null,
1164
+ `status: ${entry.status}`,
1165
+ entry.lastHandoffSummary ? `handoff: ${entry.lastHandoffSummary.slice(0, 120)}` : null,
1166
+ entry.resumeFailureReason ? `last failure: ${entry.resumeFailureReason.slice(0, 120)}` : null,
1167
+ ].filter((value) => Boolean(value));
1168
+ return values.length ? ` (${values.join('; ')})` : '';
1169
+ }
1170
+ function persistedReopenAuthorityWarning(reason) {
1171
+ return [
1172
+ '[Persisted subagent reopen]',
1173
+ `- Warning: persisted subagent authority was excluded (${reason}); no resume authority or bookkeeping was granted.`,
1174
+ '- Silver rule: do not spawn a same-role/same-lane replacement solely because persisted reopen authority was unavailable; continue in the root or another compatible existing lane.',
1175
+ ].join('\n');
1176
+ }
1177
+ function persistedReopenLegacyNotice(excludedCount, source) {
1178
+ return [
1179
+ '[Persisted subagent reopen]',
1180
+ `- SessionStart source: ${source}; saved subagent ids found: 0.`,
1181
+ `- Notice: ${excludedCount} persisted subagent record(s) were excluded as non-authoritative or legacy; no resume authority or bookkeeping was granted.`,
1182
+ '- Silver rule: do not spawn a same-role/same-lane replacement solely because persisted reopen authority was unavailable; continue in the root or another compatible existing lane.',
1183
+ ].join('\n');
1184
+ }
1185
+ /**
1186
+ * True when a stored partition is provably about the current root, and not an
1187
+ * unrelated/historical workspace session that merely happens to contain a
1188
+ * thread id equal to the current canonical session key. Canonical-key collision
1189
+ * quarantine is scoped to correlated partitions so it cannot destroy authority
1190
+ * or resume bookkeeping belonging to a different root.
1191
+ */
1192
+ function partitionCorrelatesToRoot(session, sessionId, rootNativeSessionId) {
1193
+ if (session.session_id === sessionId || session.session_id === rootNativeSessionId)
1194
+ return true;
1195
+ if (session.leader_thread_id === rootNativeSessionId)
1196
+ return true;
1197
+ if (session.threads[rootNativeSessionId] !== undefined)
1198
+ return true;
1199
+ return Object.values(session.threads).some((thread) => thread.direct_child_root_id === rootNativeSessionId
1200
+ || thread.direct_child_parent_id === rootNativeSessionId);
1201
+ }
1202
+ /**
1203
+ * Repair persisted root-as-subagent identity inversion inside an already-parsed
1204
+ * state. Only a thread whose id equals the exact native root id is conclusively
1205
+ * the root and is reclassified as leader. A thread whose id equals only the
1206
+ * canonical session key is always ambiguous — the storage key is not identity
1207
+ * proof — so it is never destructively reclassified: authority-bearing records
1208
+ * are revoked (fail closed) and every ambiguous record loses resume bookkeeping.
1209
+ * That ambiguity quarantine only applies inside partitions that correlate to
1210
+ * this root; unrelated partitions are left untouched.
1211
+ * Returns true when any record changed.
1212
+ */
1213
+ function repairPersistedRootIdentityInState(state, sessionId, rootNativeSessionId, repairTimestamp) {
1214
+ let changed = false;
1215
+ for (const correlatedSession of Object.values(state.sessions)) {
1216
+ let sessionChanged = false;
1217
+ const correlated = partitionCorrelatesToRoot(correlatedSession, sessionId, rootNativeSessionId);
1218
+ for (const rootId of new Set([sessionId, rootNativeSessionId])) {
1219
+ const rootThread = correlatedSession.threads[rootId];
1220
+ if (!rootThread || rootThread.kind !== 'subagent')
1221
+ continue;
1222
+ const ambiguousCanonicalCollision = rootId === sessionId && sessionId !== rootNativeSessionId;
1223
+ if (ambiguousCanonicalCollision) {
1224
+ // An uncorrelated partition's identically-named thread belongs to a
1225
+ // different root: leave its authority and bookkeeping alone.
1226
+ if (!correlated)
1227
+ continue;
1228
+ const carriesAuthority = rootThread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
1229
+ || rootThread.direct_child_root_id !== undefined
1230
+ || rootThread.direct_child_parent_id !== undefined;
1231
+ if (carriesAuthority && rootThread.reopen_authority_revoked !== true) {
1232
+ // Fail closed without rewriting identity: the id collides with the
1233
+ // canonical root key but carries child-shaped authority evidence, so
1234
+ // it stays a subagent whose reopen authority is revoked until root
1235
+ // identity is independently established.
1236
+ rootThread.reopen_authority_revoked = true;
1237
+ rootThread.reopen_authority_conflict_reason = 'canonical_root_collision';
1238
+ rootThread.reopen_authority_conflict_at = repairTimestamp;
1239
+ sessionChanged = true;
1240
+ }
1241
+ if (rootThread.resume_requested_at !== undefined
1242
+ || rootThread.resume_completed_at !== undefined
1243
+ || rootThread.resume_failed_at !== undefined
1244
+ || rootThread.resume_failure_reason !== undefined) {
1245
+ delete rootThread.resume_requested_at;
1246
+ delete rootThread.resume_completed_at;
1247
+ delete rootThread.resume_failed_at;
1248
+ delete rootThread.resume_failure_reason;
1249
+ sessionChanged = true;
1250
+ }
1251
+ continue;
1252
+ }
1253
+ if (rootId !== rootNativeSessionId)
1254
+ continue;
1255
+ rootThread.kind = 'leader';
1256
+ delete rootThread.direct_child_root_id;
1257
+ delete rootThread.direct_child_parent_id;
1258
+ delete rootThread.reopen_authority_revoked;
1259
+ delete rootThread.reopen_authority_conflict_reason;
1260
+ delete rootThread.reopen_authority_conflict_at;
1261
+ delete rootThread.resume_requested_at;
1262
+ delete rootThread.resume_completed_at;
1263
+ delete rootThread.resume_failed_at;
1264
+ delete rootThread.resume_failure_reason;
1265
+ sessionChanged = true;
1266
+ }
1267
+ if (correlatedSession.threads[rootNativeSessionId] && correlatedSession.leader_thread_id !== rootNativeSessionId) {
1268
+ correlatedSession.leader_thread_id = rootNativeSessionId;
1269
+ sessionChanged = true;
1270
+ }
1271
+ if (sessionChanged) {
1272
+ correlatedSession.updated_at = repairTimestamp;
1273
+ changed = true;
1274
+ }
1275
+ }
1276
+ return changed;
1277
+ }
1278
+ /**
1279
+ * Pointer-authoritative root identity repair, decoupled from reopen candidate
1280
+ * output. Runs under the tracker lock and fails closed on malformed bytes.
1281
+ */
1282
+ export function repairPersistedRootIdentity(cwd, context) {
1283
+ const sessionId = context.sessionId.trim();
1284
+ const rootNativeSessionId = context.rootNativeSessionId.trim();
1285
+ if (!sessionId || !rootNativeSessionId)
1286
+ return false;
1287
+ const path = subagentTrackingPath(cwd);
1288
+ return withCrossProcessFileLockSync(path, (lock) => {
1289
+ let raw;
1290
+ try {
1291
+ raw = readFileSync(path, 'utf-8');
1292
+ }
1293
+ catch {
1294
+ return false;
1295
+ }
1296
+ const state = parseStrictSubagentTrackingState(raw);
1297
+ if (!state)
1298
+ return false;
1299
+ const changed = repairPersistedRootIdentityInState(state, sessionId, rootNativeSessionId, new Date().toISOString());
1300
+ if (changed) {
1301
+ lock.assertOwnership();
1302
+ writeSubagentTrackingStateSync(cwd, state, lock.publish);
1303
+ }
1304
+ return changed;
1305
+ });
1306
+ }
1307
+ /**
1308
+ * Quarantine reopen authority for a root that is persisted as a subagent when
1309
+ * the identity evidence is contradictory (for example a self-parented
1310
+ * transcript marker). Unlike full repair this never asserts leader identity, so
1311
+ * legitimate descriptive subagent evidence is preserved; it only strips the
1312
+ * authority and resume bookkeeping that a root must never carry. Returns true
1313
+ * when any record changed.
1314
+ */
1315
+ export function quarantinePersistedRootAuthority(cwd, context) {
1316
+ const sessionId = context.sessionId.trim();
1317
+ const rootNativeSessionId = context.rootNativeSessionId.trim();
1318
+ if (!sessionId || !rootNativeSessionId)
1319
+ return false;
1320
+ const path = subagentTrackingPath(cwd);
1321
+ return withCrossProcessFileLockSync(path, (lock) => {
1322
+ let raw;
1323
+ try {
1324
+ raw = readFileSync(path, 'utf-8');
1325
+ }
1326
+ catch {
1327
+ return false;
1328
+ }
1329
+ const state = parseStrictSubagentTrackingState(raw);
1330
+ if (!state)
1331
+ return false;
1332
+ const timestamp = new Date().toISOString();
1333
+ let changed = false;
1334
+ for (const session of Object.values(state.sessions)) {
1335
+ if (!partitionCorrelatesToRoot(session, sessionId, rootNativeSessionId))
1336
+ continue;
1337
+ const rootThread = session.threads[rootNativeSessionId];
1338
+ if (!rootThread || rootThread.kind !== 'subagent')
1339
+ continue;
1340
+ const carriesAuthority = rootThread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
1341
+ || rootThread.direct_child_root_id !== undefined
1342
+ || rootThread.direct_child_parent_id !== undefined;
1343
+ let sessionChanged = false;
1344
+ if (carriesAuthority && rootThread.reopen_authority_revoked !== true) {
1345
+ rootThread.reopen_authority_revoked = true;
1346
+ rootThread.reopen_authority_conflict_reason = 'contradictory_root_child_evidence';
1347
+ rootThread.reopen_authority_conflict_at = timestamp;
1348
+ sessionChanged = true;
1349
+ }
1350
+ if (rootThread.resume_requested_at !== undefined
1351
+ || rootThread.resume_completed_at !== undefined
1352
+ || rootThread.resume_failed_at !== undefined
1353
+ || rootThread.resume_failure_reason !== undefined) {
1354
+ delete rootThread.resume_requested_at;
1355
+ delete rootThread.resume_completed_at;
1356
+ delete rootThread.resume_failed_at;
1357
+ delete rootThread.resume_failure_reason;
1358
+ sessionChanged = true;
1359
+ }
1360
+ if (sessionChanged) {
1361
+ session.updated_at = timestamp;
1362
+ changed = true;
1363
+ }
1364
+ }
1365
+ if (changed) {
1366
+ lock.assertOwnership();
1367
+ writeSubagentTrackingStateSync(cwd, state, lock.publish);
1368
+ }
1369
+ return changed;
1370
+ });
1371
+ }
1372
+ /** Strict, lock-serialized authority consumption for SessionStart persisted reopen output. */
1373
+ export function consumeDirectChildReopenContext(cwd, context) {
1374
+ const sessionId = context.sessionId.trim();
1375
+ const rootNativeSessionId = context.rootNativeSessionId.trim();
1376
+ if (!sessionId || !rootNativeSessionId)
1377
+ return null;
1378
+ const path = subagentTrackingPath(cwd);
1379
+ return withCrossProcessFileLockSync(path, (lock) => {
1380
+ let raw;
1381
+ try {
1382
+ raw = readFileSync(path, 'utf-8');
1383
+ }
1384
+ catch (error) {
1385
+ if (error.code === 'ENOENT')
1386
+ return null;
1387
+ return persistedReopenAuthorityWarning('tracker_read_failed');
1388
+ }
1389
+ const state = parseStrictSubagentTrackingState(raw);
1390
+ if (!state)
1391
+ return persistedReopenAuthorityWarning('malformed_tracker_state');
1392
+ // A revocation that could not be published leaves a durable fence. Fenced
1393
+ // ids stay denied here even though the on-disk record still looks valid,
1394
+ // so a failed negative observation can never leave old authority usable.
1395
+ const fence = readAuthorityFence(cwd);
1396
+ if (!fence.ok)
1397
+ return persistedReopenAuthorityWarning('unreadable_authority_fence');
1398
+ const repairTimestamp = new Date().toISOString();
1399
+ // Root identity repair runs before candidate eligibility so a missing
1400
+ // canonical partition cannot leave a known root inversion unrepaired.
1401
+ const changed = repairPersistedRootIdentityInState(state, sessionId, rootNativeSessionId, repairTimestamp);
1402
+ const session = state.sessions[sessionId];
1403
+ if (!session) {
1404
+ if (changed) {
1405
+ lock.assertOwnership();
1406
+ writeSubagentTrackingStateSync(cwd, state, lock.publish);
1407
+ }
1408
+ return null;
1409
+ }
1410
+ const savedSubagents = Object.values(session.threads)
1411
+ .filter((thread) => {
1412
+ if (thread.kind !== 'subagent'
1413
+ || thread.thread_id === sessionId
1414
+ || thread.thread_id === rootNativeSessionId
1415
+ || session.leader_thread_id === thread.thread_id
1416
+ || fence.ids.has(thread.thread_id)
1417
+ || thread.provenance_kind !== NATIVE_SUBAGENT_PROVENANCE
1418
+ || thread.direct_child_root_id !== rootNativeSessionId
1419
+ || thread.direct_child_parent_id !== rootNativeSessionId
1420
+ || thread.reopen_authority_revoked === true)
1421
+ return false;
1422
+ return Object.values(state.sessions).every((correlationSession) => {
1423
+ const view = correlationSession.threads[thread.thread_id];
1424
+ if (!view)
1425
+ return true;
1426
+ // Recognized legacy adapted records are descriptive-only: they never
1427
+ // grant authority and never block a newly attested valid child. The
1428
+ // tolerance is shape-constrained: an adapted view may only waive
1429
+ // missing attestation for a non-leader subagent view and never waives
1430
+ // leader-kind or leader_thread_id contradictions.
1431
+ if (view.provenance_kind === DESCRIPTIVE_ADAPTED_PROVENANCE
1432
+ && view.kind === 'subagent'
1433
+ && correlationSession.leader_thread_id !== thread.thread_id)
1434
+ return true;
1435
+ return correlationSession.leader_thread_id !== thread.thread_id
1436
+ && view.kind === 'subagent'
1437
+ && view.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
1438
+ && view.direct_child_root_id === rootNativeSessionId
1439
+ && view.direct_child_parent_id === rootNativeSessionId
1440
+ && view.reopen_authority_revoked !== true;
1441
+ });
1442
+ })
1443
+ .map((thread) => normalizeLedgerEntry(thread, thread.status ?? 'closed'))
1444
+ .sort(compareResumeEntries);
1445
+ if (!savedSubagents.length) {
1446
+ if (changed) {
1447
+ session.updated_at = new Date().toISOString();
1448
+ lock.assertOwnership();
1449
+ writeSubagentTrackingStateSync(cwd, state, lock.publish);
1450
+ }
1451
+ // Surface intentional exclusion of legacy/non-authoritative records instead
1452
+ // of silently returning no context. The notice fires only when every
1453
+ // relevant view across every correlation partition is genuinely
1454
+ // descriptive/legacy: any authority-bearing, leader-kind, or
1455
+ // leader_thread_id-colliding view keeps the denial fail-closed null.
1456
+ const excludedIds = new Set(Object.values(session.threads)
1457
+ .filter((thread) => thread.kind === 'subagent')
1458
+ .map((thread) => thread.thread_id));
1459
+ const everyRelevantViewIsLegacy = excludedIds.size > 0
1460
+ && Object.values(state.sessions).every((correlationSession) => {
1461
+ if (correlationSession.leader_thread_id && excludedIds.has(correlationSession.leader_thread_id))
1462
+ return false;
1463
+ return [...excludedIds].every((threadId) => {
1464
+ const view = correlationSession.threads[threadId];
1465
+ if (!view)
1466
+ return true;
1467
+ return view.kind === 'subagent'
1468
+ && view.provenance_kind !== NATIVE_SUBAGENT_PROVENANCE
1469
+ && view.direct_child_root_id === undefined
1470
+ && view.direct_child_parent_id === undefined
1471
+ && view.reopen_authority_revoked !== true;
1472
+ });
1473
+ });
1474
+ if (everyRelevantViewIsLegacy) {
1475
+ return persistedReopenLegacyNotice(excludedIds.size, context.source);
1476
+ }
1477
+ return null;
1478
+ }
1479
+ const reopenTargets = savedSubagents.filter((entry) => entry.status !== 'unavailable');
1480
+ const unavailableTargets = savedSubagents.filter((entry) => entry.status === 'unavailable');
1481
+ const failedTargets = savedSubagents.filter((entry) => entry.resumeFailedAt || entry.resumeFailureReason);
1482
+ const now = new Date().toISOString();
1483
+ for (const target of reopenTargets) {
1484
+ session.threads[target.threadId] = { ...session.threads[target.threadId], resume_requested_at: now };
1485
+ }
1486
+ if (reopenTargets.length || changed) {
1487
+ session.updated_at = now;
1488
+ lock.assertOwnership();
1489
+ writeSubagentTrackingStateSync(cwd, state, lock.publish);
1490
+ }
1491
+ const lines = [
1492
+ '[Persisted subagent reopen]',
1493
+ `- SessionStart source: ${context.source}; saved subagent ids found: ${savedSubagents.length}.`,
1494
+ ];
1495
+ if (reopenTargets.length) {
1496
+ lines.push('- Reopen these persisted subagents by id before continuing work or spawning any same-role/same-lane replacement:');
1497
+ lines.push(...reopenTargets.slice(0, 12).map((entry) => ` - resume_agent(${JSON.stringify(entry.agentId)})${formatReopenMetadata(entry)}`));
1498
+ if (reopenTargets.length > 12)
1499
+ lines.push(` - ... ${reopenTargets.length - 12} more saved subagent id(s) omitted from this compact SessionStart context; consult .omx/state/subagent-tracking.json before spawning replacements.`);
1500
+ }
1501
+ else {
1502
+ lines.push('- No compatible saved subagent id is currently marked reopenable; do not spawn a replacement merely because reopen was unavailable.');
1503
+ }
1504
+ lines.push('- Silver rule: when follow-up work targets an existing role/lane, reuse the matching reopened id; avoid duplicate same-type subagent spawns.');
1505
+ lines.push('- If resume_agent fails, surface a clear warning with the id and reason, then continue in the root or another compatible existing lane; do not spawn a new agent solely because reopen failed.');
1506
+ const warnings = [...new Map([...unavailableTargets, ...failedTargets].map((entry) => [entry.agentId, entry])).values()];
1507
+ if (warnings.length) {
1508
+ lines.push('- Reopen warnings:');
1509
+ lines.push(...warnings.slice(0, 8).map((entry) => ` - ${entry.agentId}${formatReopenMetadata(entry)}`));
1510
+ if (warnings.length > 8)
1511
+ lines.push(` - ... ${warnings.length - 8} more warning(s) omitted from this compact SessionStart context.`);
1512
+ }
1513
+ return lines.join('\n');
1514
+ });
1515
+ }
1337
1516
  export function summarizeSubagentSession(state, sessionId, options = {}) {
1338
1517
  const normalized = normalizeSubagentTrackingState(state);
1339
1518
  const session = normalized.sessions[sessionId];