borgmcp 1.1.15 → 2.0.1

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 (459) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts.map +1 -0
  71. package/dist/codex-app-wake.js +432 -2
  72. package/dist/codex-app-wake.js.map +1 -0
  73. package/dist/codex-launch.d.ts.map +1 -0
  74. package/dist/codex-launch.js +102 -2
  75. package/dist/codex-launch.js.map +1 -0
  76. package/dist/codex-remote.d.ts +6 -1
  77. package/dist/codex-remote.d.ts.map +1 -0
  78. package/dist/codex-remote.js +394 -1
  79. package/dist/codex-remote.js.map +1 -0
  80. package/dist/codex-wake-resolve.d.ts.map +1 -0
  81. package/dist/codex-wake-resolve.js +127 -1
  82. package/dist/codex-wake-resolve.js.map +1 -0
  83. package/dist/config-utils.d.ts +2 -2
  84. package/dist/config-utils.d.ts.map +1 -0
  85. package/dist/config-utils.js +766 -3
  86. package/dist/config-utils.js.map +1 -0
  87. package/dist/config.d.ts +98 -61
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +426 -1
  90. package/dist/config.js.map +1 -0
  91. package/dist/console-prefix.d.ts.map +1 -0
  92. package/dist/console-prefix.js +96 -1
  93. package/dist/console-prefix.js.map +1 -0
  94. package/dist/cube-name.d.ts.map +1 -0
  95. package/dist/cube-name.js +67 -1
  96. package/dist/cube-name.js.map +1 -0
  97. package/dist/cubes.d.ts +126 -18
  98. package/dist/cubes.d.ts.map +1 -0
  99. package/dist/cubes.js +412 -4
  100. package/dist/cubes.js.map +1 -0
  101. package/dist/debug.d.ts.map +1 -0
  102. package/dist/debug.js +71 -1
  103. package/dist/debug.js.map +1 -0
  104. package/dist/direct-log.d.ts.map +1 -0
  105. package/dist/direct-log.js +11 -1
  106. package/dist/direct-log.js.map +1 -0
  107. package/dist/docs-sections.d.ts +11 -10
  108. package/dist/docs-sections.d.ts.map +1 -0
  109. package/dist/docs-sections.js +113 -3
  110. package/dist/docs-sections.js.map +1 -0
  111. package/dist/drone-lifecycle.d.ts +5 -21
  112. package/dist/drone-lifecycle.d.ts.map +1 -0
  113. package/dist/drone-lifecycle.js +57 -8
  114. package/dist/drone-lifecycle.js.map +1 -0
  115. package/dist/drone-management.d.ts +29 -0
  116. package/dist/drone-management.d.ts.map +1 -0
  117. package/dist/drone-management.js +109 -0
  118. package/dist/drone-management.js.map +1 -0
  119. package/dist/ensure-mcp-config.d.ts.map +1 -0
  120. package/dist/ensure-mcp-config.js +36 -1
  121. package/dist/ensure-mcp-config.js.map +1 -0
  122. package/dist/evict-drone.d.ts +10 -2
  123. package/dist/evict-drone.d.ts.map +1 -0
  124. package/dist/evict-drone.js +60 -1
  125. package/dist/evict-drone.js.map +1 -0
  126. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  127. package/dist/gc-orphan-inboxes.js +177 -1
  128. package/dist/gc-orphan-inboxes.js.map +1 -0
  129. package/dist/inbox-monitor.d.ts +1 -1
  130. package/dist/inbox-monitor.d.ts.map +1 -0
  131. package/dist/inbox-monitor.js +856 -3
  132. package/dist/inbox-monitor.js.map +1 -0
  133. package/dist/index.d.ts +6 -4
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +1034 -37
  136. package/dist/index.js.map +1 -0
  137. package/dist/launch-all-cmd.d.ts.map +1 -0
  138. package/dist/launch-all-cmd.js +377 -37
  139. package/dist/launch-all-cmd.js.map +1 -0
  140. package/dist/launch-all-command.d.ts.map +1 -0
  141. package/dist/launch-all-command.js +24 -1
  142. package/dist/launch-all-command.js.map +1 -0
  143. package/dist/launch-all-deps.d.ts +3 -8
  144. package/dist/launch-all-deps.d.ts.map +1 -0
  145. package/dist/launch-all-deps.js +99 -1
  146. package/dist/launch-all-deps.js.map +1 -0
  147. package/dist/launch-all-discovery.d.ts +1 -0
  148. package/dist/launch-all-discovery.d.ts.map +1 -0
  149. package/dist/launch-all-discovery.js +90 -4
  150. package/dist/launch-all-discovery.js.map +1 -0
  151. package/dist/launch-all-locks.d.ts.map +1 -0
  152. package/dist/launch-all-locks.js +54 -1
  153. package/dist/launch-all-locks.js.map +1 -0
  154. package/dist/launch-gate.d.ts +2 -2
  155. package/dist/launch-gate.d.ts.map +1 -0
  156. package/dist/launch-gate.js +73 -1
  157. package/dist/launch-gate.js.map +1 -0
  158. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  159. package/dist/lifecycle-log-guard.js +93 -2
  160. package/dist/lifecycle-log-guard.js.map +1 -0
  161. package/dist/list-roles-render.d.ts.map +1 -0
  162. package/dist/list-roles-render.js +40 -6
  163. package/dist/list-roles-render.js.map +1 -0
  164. package/dist/local-log-routing.d.ts +19 -0
  165. package/dist/local-log-routing.d.ts.map +1 -0
  166. package/dist/local-log-routing.js +67 -0
  167. package/dist/local-log-routing.js.map +1 -0
  168. package/dist/local-manage-tool-result.d.ts +8 -0
  169. package/dist/local-manage-tool-result.d.ts.map +1 -0
  170. package/dist/local-manage-tool-result.js +10 -0
  171. package/dist/local-manage-tool-result.js.map +1 -0
  172. package/dist/local-server-cursor.d.ts +34 -0
  173. package/dist/local-server-cursor.d.ts.map +1 -0
  174. package/dist/local-server-cursor.js +151 -0
  175. package/dist/local-server-cursor.js.map +1 -0
  176. package/dist/log-audit.d.ts.map +1 -0
  177. package/dist/log-audit.js +194 -3
  178. package/dist/log-audit.js.map +1 -0
  179. package/dist/log-stream.d.ts +31 -18
  180. package/dist/log-stream.d.ts.map +1 -0
  181. package/dist/log-stream.js +1214 -12
  182. package/dist/log-stream.js.map +1 -0
  183. package/dist/model-presets.d.ts.map +1 -0
  184. package/dist/model-presets.js +19 -1
  185. package/dist/model-presets.js.map +1 -0
  186. package/dist/name-validator.d.ts.map +1 -0
  187. package/dist/name-validator.js +22 -1
  188. package/dist/name-validator.js.map +1 -0
  189. package/dist/opencode-drone.d.ts.map +1 -0
  190. package/dist/opencode-drone.js +438 -4
  191. package/dist/opencode-drone.js.map +1 -0
  192. package/dist/opencode-plugin.d.ts +1 -1
  193. package/dist/opencode-plugin.d.ts.map +1 -0
  194. package/dist/opencode-plugin.js +29 -4
  195. package/dist/opencode-plugin.js.map +1 -0
  196. package/dist/parse-assimilate-args.d.ts +1 -1
  197. package/dist/parse-assimilate-args.d.ts.map +1 -0
  198. package/dist/parse-assimilate-args.js +125 -1
  199. package/dist/parse-assimilate-args.js.map +1 -0
  200. package/dist/parse-launch-all-args.d.ts.map +1 -0
  201. package/dist/parse-launch-all-args.js +70 -1
  202. package/dist/parse-launch-all-args.js.map +1 -0
  203. package/dist/postinstall-banner.d.ts.map +1 -0
  204. package/dist/postinstall-banner.js +27 -2
  205. package/dist/postinstall-banner.js.map +1 -0
  206. package/dist/postinstall.d.ts.map +1 -0
  207. package/dist/postinstall.js +32 -5
  208. package/dist/postinstall.js.map +1 -0
  209. package/dist/readiness-probe.d.ts.map +1 -0
  210. package/dist/readiness-probe.js +9 -1
  211. package/dist/readiness-probe.js.map +1 -0
  212. package/dist/regen-format.d.ts.map +1 -0
  213. package/dist/regen-format.js +545 -13
  214. package/dist/regen-format.js.map +1 -0
  215. package/dist/regen.d.ts.map +1 -0
  216. package/dist/regen.js +141 -5
  217. package/dist/regen.js.map +1 -0
  218. package/dist/remote-client.d.ts +65 -175
  219. package/dist/remote-client.d.ts.map +1 -0
  220. package/dist/remote-client.js +942 -1
  221. package/dist/remote-client.js.map +1 -0
  222. package/dist/reset-local-seat-cmd.d.ts +50 -0
  223. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  224. package/dist/reset-local-seat-cmd.js +169 -0
  225. package/dist/reset-local-seat-cmd.js.map +1 -0
  226. package/dist/role-match.d.ts.map +1 -0
  227. package/dist/role-match.js +23 -1
  228. package/dist/role-match.js.map +1 -0
  229. package/dist/role-resolver.d.ts.map +1 -0
  230. package/dist/role-resolver.js +60 -1
  231. package/dist/role-resolver.js.map +1 -0
  232. package/dist/roster-render.d.ts.map +1 -0
  233. package/dist/roster-render.js +121 -3
  234. package/dist/roster-render.js.map +1 -0
  235. package/dist/seat-probe.d.ts +32 -13
  236. package/dist/seat-probe.d.ts.map +1 -0
  237. package/dist/seat-probe.js +78 -1
  238. package/dist/seat-probe.js.map +1 -0
  239. package/dist/seat-store.d.ts +76 -0
  240. package/dist/seat-store.d.ts.map +1 -0
  241. package/dist/seat-store.js +309 -0
  242. package/dist/seat-store.js.map +1 -0
  243. package/dist/seats.d.ts +303 -0
  244. package/dist/seats.d.ts.map +1 -0
  245. package/dist/seats.js +551 -0
  246. package/dist/seats.js.map +1 -0
  247. package/dist/self-path.d.ts +27 -0
  248. package/dist/self-path.d.ts.map +1 -0
  249. package/dist/self-path.js +43 -0
  250. package/dist/self-path.js.map +1 -0
  251. package/dist/server-endpoint.d.ts +9 -0
  252. package/dist/server-endpoint.d.ts.map +1 -0
  253. package/dist/server-endpoint.js +38 -0
  254. package/dist/server-endpoint.js.map +1 -0
  255. package/dist/server-errors.d.ts +50 -0
  256. package/dist/server-errors.d.ts.map +1 -0
  257. package/dist/server-errors.js +79 -0
  258. package/dist/server-errors.js.map +1 -0
  259. package/dist/server-handshake.d.ts +197 -0
  260. package/dist/server-handshake.d.ts.map +1 -0
  261. package/dist/server-handshake.js +505 -0
  262. package/dist/server-handshake.js.map +1 -0
  263. package/dist/server-response.d.ts +3 -0
  264. package/dist/server-response.d.ts.map +1 -0
  265. package/dist/server-response.js +67 -0
  266. package/dist/server-response.js.map +1 -0
  267. package/dist/server-trust.d.ts +14 -0
  268. package/dist/server-trust.d.ts.map +1 -0
  269. package/dist/server-trust.js +225 -0
  270. package/dist/server-trust.js.map +1 -0
  271. package/dist/setup-confirm.d.ts +3 -3
  272. package/dist/setup-confirm.d.ts.map +1 -0
  273. package/dist/setup-confirm.js +126 -2
  274. package/dist/setup-confirm.js.map +1 -0
  275. package/dist/setup.d.ts +5 -3
  276. package/dist/setup.d.ts.map +1 -0
  277. package/dist/setup.js +169 -46
  278. package/dist/setup.js.map +1 -0
  279. package/dist/shell-escape.d.ts.map +1 -0
  280. package/dist/shell-escape.js +22 -1
  281. package/dist/shell-escape.js.map +1 -0
  282. package/dist/spawn.d.ts +2 -2
  283. package/dist/spawn.d.ts.map +1 -0
  284. package/dist/spawn.js +29 -10
  285. package/dist/spawn.js.map +1 -0
  286. package/dist/startup-services.d.ts +0 -1
  287. package/dist/startup-services.d.ts.map +1 -0
  288. package/dist/startup-services.js +26 -1
  289. package/dist/startup-services.js.map +1 -0
  290. package/dist/stream-owner.d.ts +6 -2
  291. package/dist/stream-owner.d.ts.map +1 -0
  292. package/dist/stream-owner.js +547 -2
  293. package/dist/stream-owner.js.map +1 -0
  294. package/dist/stream-status.d.ts +1 -1
  295. package/dist/stream-status.d.ts.map +1 -0
  296. package/dist/stream-status.js +268 -3
  297. package/dist/stream-status.js.map +1 -0
  298. package/dist/sync-roles-render.d.ts.map +1 -0
  299. package/dist/sync-roles-render.js +118 -3
  300. package/dist/sync-roles-render.js.map +1 -0
  301. package/dist/sync.d.ts +1 -1
  302. package/dist/sync.d.ts.map +1 -0
  303. package/dist/sync.js +286 -22
  304. package/dist/sync.js.map +1 -0
  305. package/dist/terminal-title.d.ts.map +1 -0
  306. package/dist/terminal-title.js +68 -1
  307. package/dist/terminal-title.js.map +1 -0
  308. package/dist/token-store.d.ts +17 -111
  309. package/dist/token-store.d.ts.map +1 -0
  310. package/dist/token-store.js +81 -1
  311. package/dist/token-store.js.map +1 -0
  312. package/dist/tool-manifest.d.ts +4 -8
  313. package/dist/tool-manifest.d.ts.map +1 -0
  314. package/dist/tool-manifest.js +554 -1
  315. package/dist/tool-manifest.js.map +1 -0
  316. package/dist/tool-scope.d.ts +8 -12
  317. package/dist/tool-scope.d.ts.map +1 -0
  318. package/dist/tool-scope.js +97 -1
  319. package/dist/tool-scope.js.map +1 -0
  320. package/dist/types.d.ts +1 -5
  321. package/dist/types.d.ts.map +1 -0
  322. package/dist/types.js +5 -0
  323. package/dist/types.js.map +1 -0
  324. package/dist/unknown-subcommand.d.ts +1 -1
  325. package/dist/unknown-subcommand.d.ts.map +1 -0
  326. package/dist/unknown-subcommand.js +37 -1
  327. package/dist/unknown-subcommand.js.map +1 -0
  328. package/dist/version.d.ts +1 -1
  329. package/dist/version.d.ts.map +1 -0
  330. package/dist/version.js +78 -2
  331. package/dist/version.js.map +1 -0
  332. package/dist/working-repo.d.ts.map +1 -0
  333. package/dist/working-repo.js +81 -1
  334. package/dist/working-repo.js.map +1 -0
  335. package/dist/worktree-lifecycle.d.ts.map +1 -0
  336. package/dist/worktree-lifecycle.js +205 -2
  337. package/dist/worktree-lifecycle.js.map +1 -0
  338. package/docs/EXTRACTION_PROVENANCE.md +40 -0
  339. package/docs/LOCAL_SERVER.md +103 -0
  340. package/docs/RELEASING.md +120 -0
  341. package/package.json +52 -34
  342. package/src/agent-runtime.ts +69 -0
  343. package/src/assimilate-cmd.ts +1864 -0
  344. package/src/assimilate-deps.ts +444 -0
  345. package/src/assimilate-guard.ts +84 -0
  346. package/src/assimilate-welcome.ts +39 -0
  347. package/src/auth-env.ts +21 -0
  348. package/src/backends/launch-all-pastelist.ts +18 -0
  349. package/src/backends/launch-all-tmux.ts +75 -0
  350. package/src/backends/launch-all-windows.ts +110 -0
  351. package/src/bare-launch-menu.ts +148 -0
  352. package/src/claude-launch-args.ts +35 -0
  353. package/src/claude.ts +561 -0
  354. package/src/cleanup-cmd.ts +581 -0
  355. package/src/clear-rewake-core.ts +34 -0
  356. package/src/clear-rewake.ts +18 -0
  357. package/src/cli-help.ts +116 -0
  358. package/src/cli-platform.ts +127 -0
  359. package/src/cli-tool-approval.ts +716 -0
  360. package/src/codex-app-server.ts +238 -0
  361. package/src/codex-app-wake.ts +512 -0
  362. package/src/codex-launch.ts +140 -0
  363. package/src/codex-remote.ts +500 -0
  364. package/src/codex-wake-resolve.ts +155 -0
  365. package/src/config-utils.ts +811 -0
  366. package/src/config.ts +595 -0
  367. package/src/console-prefix.ts +99 -0
  368. package/src/cube-name.ts +69 -0
  369. package/src/cubes.ts +583 -0
  370. package/src/debug.ts +73 -0
  371. package/src/direct-log.ts +9 -0
  372. package/src/docs-sections.ts +129 -0
  373. package/src/drone-lifecycle.ts +62 -0
  374. package/src/drone-management.ts +162 -0
  375. package/src/ensure-mcp-config.ts +54 -0
  376. package/src/evict-drone.ts +82 -0
  377. package/src/gc-orphan-inboxes.ts +229 -0
  378. package/src/inbox-monitor.ts +1005 -0
  379. package/src/index.ts +1201 -0
  380. package/src/launch-all-cmd.ts +450 -0
  381. package/src/launch-all-command.ts +30 -0
  382. package/src/launch-all-deps.ts +188 -0
  383. package/src/launch-all-discovery.ts +118 -0
  384. package/src/launch-all-locks.ts +74 -0
  385. package/src/launch-gate.ts +80 -0
  386. package/src/lifecycle-log-guard.ts +144 -0
  387. package/src/list-roles-render.ts +52 -0
  388. package/src/local-log-routing.ts +106 -0
  389. package/src/local-manage-tool-result.ts +10 -0
  390. package/src/local-server-cursor.ts +188 -0
  391. package/src/log-audit.ts +207 -0
  392. package/src/log-stream.ts +1505 -0
  393. package/src/model-presets.ts +22 -0
  394. package/src/name-validator.ts +23 -0
  395. package/src/opencode-drone.ts +528 -0
  396. package/src/opencode-plugin.ts +38 -0
  397. package/src/parse-assimilate-args.ts +125 -0
  398. package/src/parse-launch-all-args.ts +100 -0
  399. package/src/postinstall-banner.ts +37 -0
  400. package/src/postinstall.ts +36 -0
  401. package/src/readiness-probe.ts +12 -0
  402. package/src/regen-format.ts +651 -0
  403. package/src/regen.ts +150 -0
  404. package/src/remote-client.ts +1379 -0
  405. package/src/reset-local-seat-cmd.ts +227 -0
  406. package/src/role-match.ts +29 -0
  407. package/src/role-resolver.ts +93 -0
  408. package/src/roster-render.ts +196 -0
  409. package/src/seat-probe.ts +115 -0
  410. package/src/seat-store.ts +333 -0
  411. package/src/seats.ts +741 -0
  412. package/src/self-path.ts +51 -0
  413. package/src/server-endpoint.ts +38 -0
  414. package/src/server-errors.ts +93 -0
  415. package/src/server-handshake.ts +783 -0
  416. package/src/server-response.ts +76 -0
  417. package/src/server-trust.ts +245 -0
  418. package/src/setup-confirm.ts +171 -0
  419. package/src/setup.ts +186 -0
  420. package/src/shell-escape.ts +22 -0
  421. package/src/spawn.ts +31 -0
  422. package/src/startup-services.ts +36 -0
  423. package/src/stream-owner.ts +686 -0
  424. package/src/stream-status.ts +337 -0
  425. package/src/sync-roles-render.ts +163 -0
  426. package/src/sync.ts +363 -0
  427. package/src/terminal-title.ts +76 -0
  428. package/src/token-store.ts +102 -0
  429. package/src/tool-manifest.ts +610 -0
  430. package/src/tool-scope.ts +110 -0
  431. package/src/types.ts +5 -0
  432. package/src/unknown-subcommand.ts +35 -0
  433. package/src/version.ts +82 -0
  434. package/src/working-repo.ts +99 -0
  435. package/src/worktree-lifecycle.ts +254 -0
  436. package/dist/auth-recovery.d.ts +0 -33
  437. package/dist/auth-recovery.js +0 -1
  438. package/dist/auth.d.ts +0 -103
  439. package/dist/auth.js +0 -39
  440. package/dist/device-auth.d.ts +0 -75
  441. package/dist/device-auth.js +0 -1
  442. package/dist/drone-address.d.ts +0 -26
  443. package/dist/drone-address.js +0 -1
  444. package/dist/get-started.d.ts +0 -20
  445. package/dist/get-started.js +0 -2
  446. package/dist/health-beat.d.ts +0 -134
  447. package/dist/health-beat.js +0 -1
  448. package/dist/role-section.d.ts +0 -110
  449. package/dist/role-section.js +0 -8
  450. package/dist/setup-action.d.ts +0 -20
  451. package/dist/setup-action.js +0 -1
  452. package/dist/stale-version-check.d.ts +0 -40
  453. package/dist/stale-version-check.js +0 -1
  454. package/dist/subscription-retry.d.ts +0 -40
  455. package/dist/subscription-retry.js +0 -1
  456. package/dist/templates.d.ts +0 -107
  457. package/dist/templates.js +0 -359
  458. package/dist/token-crypto.d.ts +0 -50
  459. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,512 @@
1
+ import { getActiveCube, getCodexWakeTarget, setCodexWakeTarget } from './cubes.js';
2
+ import { CodexAppServerClient } from './codex-app-server.js';
3
+ import { checkCodexBridgeHealthy } from './codex-remote.js';
4
+ import {
5
+ BORG_CODEX_REMOTE_WAKE_ENV,
6
+ resolveSessionAgentKind,
7
+ } from './agent-runtime.js';
8
+ import {
9
+ codexAppServerSocketFromEnv,
10
+ pickFreshThread,
11
+ wakeTargetChanged,
12
+ wakeRetryBackoffMs,
13
+ wakeRetryExpired,
14
+ WAKE_RETRY_MAX_ATTEMPTS,
15
+ shouldFireHeartbeat,
16
+ type CodexThreadInfo,
17
+ } from './codex-wake-resolve.js';
18
+
19
+ export const CODEX_WAKE_PROMPT =
20
+ 'New Borg cube-log activity arrived.';
21
+
22
+ export function formatCodexWakePrompt(inboxLine: string): string {
23
+ return `New Borg cube-log activity arrived:\n${inboxLine}`;
24
+ }
25
+
26
+ // gh#708: STATIC catch-up/drain prompt (zero interpolation — no token/secret/PII/
27
+ // cube-content; the entry bodies are fetched by codex itself via the
28
+ // grant/visibility-gated borg_read-log, never injected into the wake). Delivered
29
+ // once after a wake is deferred (mid-turn thread) or retried (transient miss), to
30
+ // fire the already-shipped drain so no entry is skipped. gh#857 WI-2 reuses it as
31
+ // the periodic heartbeat prompt.
32
+ export const CODEX_CATCHUP_PROMPT =
33
+ 'Borg cube activity arrived while you were busy. Wake triage: run `borg_read-log unread_only=true` and DRAIN — repeat until the returned page is under the limit and behind_by is 0 — so no entries are skipped. Then handle actionable entries; if none, resume the prior interrupted work.';
34
+
35
+ export function isCodexRemoteWakeEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
36
+ return env[BORG_CODEX_REMOTE_WAKE_ENV] === '1';
37
+ }
38
+
39
+ export { resolveSessionAgentKind } from './agent-runtime.js';
40
+
41
+ export interface CodexWakeTarget {
42
+ enabled: boolean;
43
+ }
44
+
45
+ export function resolveCodexWakeTarget(env: NodeJS.ProcessEnv = process.env): CodexWakeTarget {
46
+ if (!isCodexRemoteWakeEnabled(env)) {
47
+ return { enabled: false };
48
+ }
49
+ return { enabled: true };
50
+ }
51
+
52
+ /**
53
+ * gh#633: resolve a codex drone's transport-agnostic "wake-path-armed" signal
54
+ * from its OWN runtime wake mechanism — the app-server bridge's process
55
+ * liveness, the codex analogue of the claude tail-F Monitor health. Fed into
56
+ * the health beat so the HOP-2 wake-path-deaf classifier reads a
57
+ * transport-agnostic armed signal instead of the claude-shaped monitor_armed
58
+ * (which is false-by-design for codex and falsely flagged them, gh#633).
59
+ *
60
+ * Tri-state (boolean|null; caller maps null→armed for false-deaf-avoidance):
61
+ * - false ONLY on a positively-dead bridge: no wake target registered (the
62
+ * bridge cannot deliver wakes), OR the app-server pid is dead.
63
+ * - true when the wake target resolves AND the app-server pid is alive.
64
+ * - null when the bridge health is indeterminate (target read or pid check
65
+ * could not resolve) → armed (don't false-flag on uncertainty).
66
+ */
67
+ export async function probeCodexBridgeArmed(
68
+ active: { cubeId: string; droneId: string },
69
+ deps: {
70
+ getCodexWakeTarget?: typeof getCodexWakeTarget;
71
+ checkBridge?: typeof checkCodexBridgeHealthy;
72
+ } = {}
73
+ ): Promise<boolean | null> {
74
+ try {
75
+ const resolve = deps.getCodexWakeTarget ?? getCodexWakeTarget;
76
+ const target = await resolve(active.cubeId, active.droneId);
77
+ // No registered wake target → the bridge cannot deliver a wake → not armed.
78
+ if (!target) return false;
79
+ const check = deps.checkBridge ?? checkCodexBridgeHealthy;
80
+ return check(target.socketPath);
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ let wakeInFlight = false;
87
+ const pendingWakeRequests: Array<{
88
+ reason: string;
89
+ deps: CodexWakeDeps;
90
+ }> = [];
91
+ const deliveredWakeKeys = new Set<string>();
92
+ const deliveredWakeKeyOrder: string[] = [];
93
+ const DELIVERED_WAKE_KEY_CAP = 100;
94
+
95
+ // gh#708/#857: a single in-flight retry-drain loop coalesces ALL wakes deferred
96
+ // (mid-turn thread) or missed (transient error) into ONE retried-until-delivered
97
+ // drain. The coalesce gate means a burst collapses to one poller, not N.
98
+ let retryDrainInFlight = false;
99
+
100
+ // gh#857 WI-2: timestamp of the last SUCCESSFUL wake delivery (per-entry OR
101
+ // retry-drain OR heartbeat). The heartbeat reads this (shouldFireHeartbeat) to
102
+ // skip when a delivery already landed inside the cadence window. Module-scoped
103
+ // because the wake path and the heartbeat run in the same MCP-client child.
104
+ let lastDeliveredAt: number | null = null;
105
+
106
+ /** gh#857 WI-2: last successful wake delivery time (for the heartbeat gate). */
107
+ export function getLastDeliveredAt(): number | null {
108
+ return lastDeliveredAt;
109
+ }
110
+
111
+ function markDelivered(deps: CodexWakeDeps): void {
112
+ lastDeliveredAt = (deps.now ?? Date.now)();
113
+ }
114
+
115
+ // gh#857 WI-2: a single-in-flight guard for the heartbeat tick (mirrors
116
+ // wakeInFlight + retryDrainInFlight). Without it, a tick that stalls in IO past
117
+ // the next interval could let a second tick read the stale (pre-markDelivered)
118
+ // lastDeliveredAt, pass its gate, and double-inject the drain — the exact
119
+ // double-fire the design forbids.
120
+ let heartbeatInFlight = false;
121
+
122
+ // gh#861 finding 1: a SINGLE module-scoped mutex serializing the resolve+inject
123
+ // critical section ACROSS all three injecting paths (per-entry wake, retry-drain,
124
+ // heartbeat tick). The per-path flags above (wakeInFlight / retryDrainInFlight /
125
+ // heartbeatInFlight) each serialize their OWN path but NOT across paths — so a
126
+ // per-entry WAKE prompt and a heartbeat/retry DRAIN prompt could land in the same
127
+ // codex thread concurrently (double-inject collision). This lock closes that
128
+ // cross-path race: a path that cannot acquire it backs off via its own retry
129
+ // mechanism (heartbeat: skip tick; retry-drain: continue loop; per-entry: schedule
130
+ // a retry-drain so the entry is not lost).
131
+ let injectInFlight = false;
132
+
133
+ function tryAcquireInjectLock(): boolean {
134
+ if (injectInFlight) return false;
135
+ injectInFlight = true;
136
+ return true;
137
+ }
138
+
139
+ function releaseInjectLock(): void {
140
+ injectInFlight = false;
141
+ }
142
+
143
+ /**
144
+ * gh#861 finding 3: a positively-dead codex app-server socket. ENOENT means the
145
+ * socket file is gone (the app-server never created it / unlinked it on exit) —
146
+ * the wake path cannot deliver, so the heartbeat timer should be torn down. Kept
147
+ * narrow (ENOENT only): a transient ECONNREFUSED during a momentary blip must NOT
148
+ * tear the backstop down.
149
+ */
150
+ function isAppServerDeadError(err: unknown): boolean {
151
+ return (err as { code?: string } | null)?.code === 'ENOENT';
152
+ }
153
+
154
+ function defaultSleep(ms: number): Promise<void> {
155
+ return new Promise((resolve) => setTimeout(resolve, ms));
156
+ }
157
+
158
+ export interface CodexWakeDeps {
159
+ getActiveCube?: typeof getActiveCube;
160
+ getCodexWakeTarget?: typeof getCodexWakeTarget;
161
+ // gh#855: self-heal write of the freshly-resolved target (write-only-on-change).
162
+ setCodexWakeTarget?: typeof setCodexWakeTarget;
163
+ createClient?: (
164
+ socketPath: string
165
+ ) => Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'loadedThreadIds' | 'close'>;
166
+ // gh#855: injectable env (live app-server socket) + cwd (thread disambiguation).
167
+ env?: NodeJS.ProcessEnv;
168
+ cwd?: () => string;
169
+ // gh#708/#857: injectable for the retry-drain loop (real timers in prod; fake in tests).
170
+ sleep?: (ms: number) => Promise<void>;
171
+ now?: () => number;
172
+ // gh#857: injectable backoff jitter (default Math.random()*500) so co-located
173
+ // sibling drones don't retry in lockstep; deterministic (0) in tests.
174
+ jitter?: () => number;
175
+ // gh#857: injectable hard iteration ceiling for the retry-drain loop (default
176
+ // WAKE_RETRY_MAX_ATTEMPTS); small values let tests prove the loop terminates
177
+ // under a non-advancing clock without running thousands of iterations.
178
+ maxAttempts?: number;
179
+ // gh#861 finding 2: lease-ownership gate for the heartbeat tick — a lease-LOSING
180
+ // duplicate child must NOT tick/inject (symmetry with the per-entry path, which
181
+ // only fires inside an SSE session that holds the stream lease). Heartbeat-only;
182
+ // when omitted the gate is skipped (direct unit calls / the per-entry + drain
183
+ // paths are already lease-silenced upstream). Production wiring passes a
184
+ // predicate reading the stream loop's current lease ownership.
185
+ isStreamOwner?: () => boolean;
186
+ // gh#861 finding 3: invoked when the heartbeat tick detects the codex app-server
187
+ // socket is positively gone (ENOENT) — the wake path is dead, so the production
188
+ // wiring tears down the heartbeat timer (re-armed when an active cube returns).
189
+ onAppServerSocketDead?: () => void;
190
+ }
191
+
192
+ /**
193
+ * gh#855: FRESH wake-target resolution. Prefer THIS drone's live app-server
194
+ * socket (pinned into the child's env at spawn) and re-resolve the loaded thread
195
+ * NOW (loadedThreadIds is re-runnable) — so a missed/stale launch probe or a
196
+ * thread change can never cause permanent deafness. Self-heals the file cache
197
+ * (write-only-on-change) so other readers (probeCodexBridgeArmed / health-beat)
198
+ * stay current. Falls back to the launch-recorded file when the env socket is
199
+ * absent (un-upgraded launch) — no regression. Returns the resolved target, or
200
+ * null (caller skips this wake; the next one retries). Does NOT keep a
201
+ * connection open — the env path opens a short-lived probe client to re-resolve
202
+ * the thread, then closes it, so the caller can dedup BEFORE opening the wake
203
+ * connection (no reconnect on an already-delivered wake).
204
+ */
205
+ function makeCodexClient(
206
+ sock: string,
207
+ deps: CodexWakeDeps
208
+ ): Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'loadedThreadIds' | 'close'> {
209
+ return deps.createClient ? deps.createClient(sock) : new CodexAppServerClient(sock);
210
+ }
211
+
212
+ async function resolveFreshCodexWakeTarget(
213
+ active: { cubeId: string; droneId: string },
214
+ deps: CodexWakeDeps
215
+ ): Promise<{ socketPath: string; threadId: string } | null> {
216
+ const envSocket = codexAppServerSocketFromEnv(deps.env ?? process.env);
217
+ if (envSocket) {
218
+ const probe = makeCodexClient(envSocket, deps);
219
+ await probe.connect();
220
+ try {
221
+ const ids = await probe.loadedThreadIds();
222
+ const summaries: CodexThreadInfo[] = [];
223
+ for (const id of ids) {
224
+ const t = await probe.readThread(id);
225
+ if (t) summaries.push({ id: t.id, cwd: t.cwd, updatedAt: t.updatedAt });
226
+ }
227
+ const threadId = pickFreshThread(summaries, { cwd: (deps.cwd ?? (() => process.cwd()))() });
228
+ if (!threadId) return null; // no loaded thread yet — next wake retries (no permanent fail)
229
+ await maybePersistWakeTarget(active, { socketPath: envSocket, threadId }, deps);
230
+ return { socketPath: envSocket, threadId };
231
+ } finally {
232
+ probe.close();
233
+ }
234
+ }
235
+
236
+ // Fallback: the launch-recorded file (un-upgraded launch / env absent) — no
237
+ // connect needed to resolve, so the caller dedups before opening any socket.
238
+ const target = await (deps.getCodexWakeTarget ?? getCodexWakeTarget)(active.cubeId, active.droneId);
239
+ if (!target) return null;
240
+ return { socketPath: target.socketPath, threadId: target.threadId };
241
+ }
242
+
243
+ /** Self-healing cache write — only when the resolved target actually changed. */
244
+ async function maybePersistWakeTarget(
245
+ active: { cubeId: string; droneId: string },
246
+ fresh: { socketPath: string; threadId: string },
247
+ deps: CodexWakeDeps
248
+ ): Promise<void> {
249
+ try {
250
+ const get = deps.getCodexWakeTarget ?? getCodexWakeTarget;
251
+ const set = deps.setCodexWakeTarget ?? setCodexWakeTarget;
252
+ const existing = await get(active.cubeId, active.droneId);
253
+ const prev = existing ? { socketPath: existing.socketPath, threadId: existing.threadId } : null;
254
+ if (wakeTargetChanged(prev, fresh)) {
255
+ await set(active.cubeId, active.droneId, fresh);
256
+ }
257
+ } catch {
258
+ // best-effort cache write; never break the wake path
259
+ }
260
+ }
261
+
262
+ export function wakeCodexViaAppServer(
263
+ reason: string = CODEX_WAKE_PROMPT,
264
+ env: NodeJS.ProcessEnv = process.env,
265
+ deps: CodexWakeDeps = {}
266
+ ): void {
267
+ const target = resolveCodexWakeTarget(env);
268
+ if (!target.enabled) return;
269
+ pendingWakeRequests.push({ reason, deps });
270
+ if (wakeInFlight) return;
271
+
272
+ wakeInFlight = true;
273
+ void drainCodexWakeQueue().finally(() => {
274
+ wakeInFlight = false;
275
+ });
276
+ }
277
+
278
+ async function drainCodexWakeQueue(): Promise<void> {
279
+ while (pendingWakeRequests.length > 0) {
280
+ const request = pendingWakeRequests.shift()!;
281
+ await wakeCodexTargeted(request.reason, request.deps);
282
+ }
283
+ }
284
+
285
+ async function wakeCodexTargeted(reason: string, deps: CodexWakeDeps): Promise<void> {
286
+ // gh#861 finding 1: another path (heartbeat/retry-drain) is mid-inject into the
287
+ // same thread — defer to the retry-drain so this entry isn't double-injected nor
288
+ // lost (the drain re-syncs the whole burst via the server read-cursor).
289
+ if (!tryAcquireInjectLock()) {
290
+ scheduleRetryDrain(deps);
291
+ return;
292
+ }
293
+ try {
294
+ const active = await (deps.getActiveCube ?? getActiveCube)();
295
+ if (!active) return;
296
+ // gh#855: resolve FRESH (live env socket + re-resolved thread), falling back
297
+ // to the launch-recorded file only when the env socket is absent.
298
+ const resolved = await resolveFreshCodexWakeTarget(active, deps);
299
+ if (!resolved) return;
300
+ const { socketPath, threadId } = resolved;
301
+ const wakeKey = `${threadId}\0${reason}`;
302
+ if (deliveredWakeKeys.has(wakeKey)) return; // dedup before opening the wake socket
303
+ const client = makeCodexClient(socketPath, deps);
304
+ await client.connect();
305
+ try {
306
+ const thread = await client.readThread(threadId);
307
+ if (thread?.status?.type === 'active') {
308
+ // gh#708/#857: the thread is mid-turn — this per-entry wake can't land
309
+ // now. Schedule the retry-drain (coalesced, retried-until-delivered) so
310
+ // the burst's entries are drained once the thread goes idle; codex has no
311
+ // on-disk tail fallback like Claude's borg-inbox-monitor.
312
+ scheduleRetryDrain(deps);
313
+ return;
314
+ }
315
+ await client.startTurn(threadId, reason);
316
+ rememberDeliveredWake(wakeKey);
317
+ markDelivered(deps);
318
+ } finally {
319
+ client.close();
320
+ }
321
+ } catch {
322
+ // gh#857: a transient connect/read/startTurn failure must NOT be silently
323
+ // swallowed (the old best-effort drop let a single blip lose an entry).
324
+ // Schedule the retry-drain so the wake is retried-until-delivered; the SSE
325
+ // stream is never broken (this is fire-and-forget).
326
+ scheduleRetryDrain(deps);
327
+ } finally {
328
+ releaseInjectLock();
329
+ }
330
+ }
331
+
332
+ /**
333
+ * gh#708/#857 WI-1: schedule a single coalesced retry-drain. Multiple wakes
334
+ * deferred (mid-turn) or missed (transient) collapse into ONE loop
335
+ * (retryDrainInFlight gate). The loop retries — with exponential backoff
336
+ * (wakeRetryBackoffMs) — until the thread is reachable+idle and the
337
+ * CODEX_CATCHUP_PROMPT drain is delivered (server read-cursor then drains ALL
338
+ * unread, so one drain covers the whole burst). Durable: unlike the old
339
+ * 15-min-give-up catch-up poller, it retries until a generous age cap
340
+ * (wakeRetryExpired); the gh#857 WI-2 heartbeat is the backstop beyond that.
341
+ * Never throws into the SSE path (fire-and-forget).
342
+ */
343
+ function scheduleRetryDrain(deps: CodexWakeDeps): void {
344
+ if (retryDrainInFlight) return; // coalesce: one loop covers all deferred/missed wakes
345
+ retryDrainInFlight = true;
346
+ void runRetryDrainLoop(deps).finally(() => {
347
+ retryDrainInFlight = false;
348
+ });
349
+ }
350
+
351
+ async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
352
+ const sleep = deps.sleep ?? defaultSleep;
353
+ const now = deps.now ?? Date.now;
354
+ const jitter = deps.jitter ?? (() => Math.random() * 500);
355
+ const maxAttempts = deps.maxAttempts ?? WAKE_RETRY_MAX_ATTEMPTS;
356
+ const startedAt = now();
357
+ let attempts = 0;
358
+ // Terminate on the time-based age cap OR a hard iteration ceiling (defensive
359
+ // belt: a non-advancing clock would make the time-only guard hot-spin).
360
+ while (!wakeRetryExpired(startedAt, now()) && attempts < maxAttempts) {
361
+ await sleep(wakeRetryBackoffMs(attempts, jitter()));
362
+ attempts++;
363
+ // gh#861 finding 1: another path (heartbeat/per-entry wake) holds the inject
364
+ // lock — back off and retry rather than double-inject into the same thread.
365
+ if (!tryAcquireInjectLock()) continue;
366
+ try {
367
+ const active = await (deps.getActiveCube ?? getActiveCube)();
368
+ if (!active) continue; // no active cube yet → keep retrying (until age cap)
369
+ // gh#855: same FRESH resolution as the per-entry wake, so a stale launch
370
+ // probe can't defeat the retry-drain either.
371
+ const resolved = await resolveFreshCodexWakeTarget(active, deps);
372
+ if (!resolved) continue; // thread not loaded yet → retry (age-capped)
373
+ const { socketPath, threadId } = resolved;
374
+ const client = makeCodexClient(socketPath, deps);
375
+ await client.connect();
376
+ try {
377
+ const thread = await client.readThread(threadId);
378
+ if (thread?.status?.type === 'active') {
379
+ continue; // re-defer: still mid-turn (backoff before next poll)
380
+ }
381
+ await client.startTurn(threadId, CODEX_CATCHUP_PROMPT);
382
+ markDelivered(deps);
383
+ return; // drain delivered → server read-cursor drains all unread → done
384
+ } finally {
385
+ client.close();
386
+ }
387
+ } catch {
388
+ // transient socket/read error must not abort the loop — keep retrying with
389
+ // backoff until reachable+idle or the age cap; never throws into SSE.
390
+ } finally {
391
+ releaseInjectLock();
392
+ }
393
+ }
394
+ // aged out: the gh#857 WI-2 periodic heartbeat is the ultimate backstop.
395
+ }
396
+
397
+ /**
398
+ * gh#857 WI-2: codex /loop-equivalent heartbeat cadence. Codex retains this
399
+ * independent 20-minute drain because it has no Claude-style per-entry inbox
400
+ * Monitor. Claude recovery is adaptive: 3h ±30m while the Monitor is healthy
401
+ * or indeterminate, and 15m ±3m only while it is explicitly broken.
402
+ */
403
+ export const CODEX_HEARTBEAT_CADENCE_MS = 20 * 60_000;
404
+
405
+ /**
406
+ * gh#857 WI-2: one tick of the codex /loop-equivalent heartbeat — a periodic,
407
+ * independent re-engagement that injects a borg_read-log (unread_only=true) DRAIN turn so an
408
+ * idle codex drone re-syncs even if every per-entry wake was missed. SKIPS when a
409
+ * delivery (per-entry wake, retry-drain, or a prior heartbeat) already landed
410
+ * within the cadence window (shouldFireHeartbeat), so an active cube with flowing
411
+ * wakes never gets a redundant injection. Unlike the per-entry path it does NOT
412
+ * consult deliveredWakeKeys — the cadence gate is the throttle, and the static
413
+ * drain prompt is intentionally re-delivered each idle window. Best-effort: a
414
+ * mid-turn thread / transient error / unresolved target just skips this tick (the
415
+ * next tick retries). Never throws.
416
+ */
417
+ export async function fireCodexHeartbeatTick(
418
+ deps: CodexWakeDeps = {},
419
+ cadenceMs: number = CODEX_HEARTBEAT_CADENCE_MS
420
+ ): Promise<void> {
421
+ if (heartbeatInFlight) return; // a prior tick's IO is still running → no overlap
422
+ const now = (deps.now ?? Date.now)();
423
+ if (!shouldFireHeartbeat(lastDeliveredAt, now, cadenceMs)) return; // a wake landed recently
424
+ // gh#861 finding 2: a lease-LOSING duplicate child must not tick/inject — mirror
425
+ // the per-entry path, which only fires inside an SSE session holding the stream
426
+ // lease. When the gate is provided and we don't own the lease, skip this tick.
427
+ if (deps.isStreamOwner && !deps.isStreamOwner()) return;
428
+ // gh#861 finding 1: another path is mid-inject into the thread → skip this tick
429
+ // (next tick retries) rather than collide a DRAIN prompt with an in-flight wake.
430
+ if (!tryAcquireInjectLock()) return;
431
+ // Set BEFORE the first await — the check+gate+set above are synchronous, so a
432
+ // concurrent tick can't interleave before the flag is set (single-threaded).
433
+ heartbeatInFlight = true;
434
+ try {
435
+ const active = await (deps.getActiveCube ?? getActiveCube)();
436
+ if (!active) return;
437
+ const resolved = await resolveFreshCodexWakeTarget(active, deps);
438
+ if (!resolved) return; // thread not loaded yet → next tick retries
439
+ const client = makeCodexClient(resolved.socketPath, deps);
440
+ await client.connect();
441
+ try {
442
+ const thread = await client.readThread(resolved.threadId);
443
+ if (thread?.status?.type === 'active') return; // mid-turn → skip; next tick retries
444
+ await client.startTurn(resolved.threadId, CODEX_CATCHUP_PROMPT);
445
+ // markDelivered updates local heartbeat-gating state only.
446
+ markDelivered(deps);
447
+ } finally {
448
+ client.close();
449
+ }
450
+ } catch (err) {
451
+ // gh#861 finding 3: a positively-dead app-server socket (ENOENT) → the wake
452
+ // path is gone; signal teardown so the timer stops ticking against a dead
453
+ // socket (re-armed when an active cube returns). Other (transient) errors are
454
+ // best-effort skips — never break the SSE stream; next tick retries.
455
+ if (isAppServerDeadError(err)) deps.onAppServerSocketDead?.();
456
+ } finally {
457
+ heartbeatInFlight = false;
458
+ releaseInjectLock();
459
+ }
460
+ }
461
+
462
+ /**
463
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat — a setInterval firing
464
+ * fireCodexHeartbeatTick every cadence. CODEX-ONLY: claude wakes via the tail-F
465
+ * inbox Monitor + /loop ScheduleWakeup and has NO app-server socket to inject
466
+ * into, so the heartbeat is intrinsically a codex mechanism. The gate reads
467
+ * agentKind and remote-wake capability LOCALLY from this child's own env,
468
+ * never a mutable/server-recorded field, so a mislabel can't silently defeat
469
+ * the backstop (gh#633 lesson). Agent CLI identity and remote transport are
470
+ * separate: a Codex CLI without a live remote transport has no app-server
471
+ * heartbeat to run. The timer is unref'd so it never keeps the process alive.
472
+ * Returns the timer, or null when this is not a remotely-wakeable Codex
473
+ * session. Injectable for tests.
474
+ */
475
+ export function startCodexHeartbeat(
476
+ opts: {
477
+ agentKind?: 'claude' | 'codex' | 'opencode';
478
+ remoteWakeEnabled?: boolean;
479
+ intervalMs?: number;
480
+ tick?: () => void;
481
+ } = {}
482
+ ): ReturnType<typeof setInterval> | null {
483
+ const agentKind = opts.agentKind ?? resolveSessionAgentKind();
484
+ const remoteWakeEnabled = opts.remoteWakeEnabled ?? isCodexRemoteWakeEnabled();
485
+ if (agentKind !== 'codex' || !remoteWakeEnabled) return null;
486
+ const intervalMs = opts.intervalMs ?? CODEX_HEARTBEAT_CADENCE_MS;
487
+ const tick = opts.tick ?? (() => void fireCodexHeartbeatTick());
488
+ const timer = setInterval(tick, intervalMs);
489
+ timer.unref?.();
490
+ return timer;
491
+ }
492
+
493
+ export function resetCodexWakeForTests(): void {
494
+ wakeInFlight = false;
495
+ pendingWakeRequests.length = 0;
496
+ deliveredWakeKeys.clear();
497
+ deliveredWakeKeyOrder.length = 0;
498
+ retryDrainInFlight = false;
499
+ lastDeliveredAt = null;
500
+ heartbeatInFlight = false;
501
+ injectInFlight = false;
502
+ }
503
+
504
+ function rememberDeliveredWake(key: string): void {
505
+ if (deliveredWakeKeys.has(key)) return;
506
+ deliveredWakeKeys.add(key);
507
+ deliveredWakeKeyOrder.push(key);
508
+ while (deliveredWakeKeyOrder.length > DELIVERED_WAKE_KEY_CAP) {
509
+ const oldKey = deliveredWakeKeyOrder.shift();
510
+ if (oldKey) deliveredWakeKeys.delete(oldKey);
511
+ }
512
+ }
@@ -0,0 +1,140 @@
1
+ import type { BorgCli } from './cubes.js';
2
+ import { wakePathArming, type AgentKind } from './regen-format.js';
3
+
4
+ /**
5
+ * The claude kickoff prompt's wake-path section (gh#929) — the SAME shared
6
+ * `wakePathArming` the SessionStart hook + /clear orientation use (one place,
7
+ * not three), plus a one-line NEVER-TaskStop safety reminder preserved from
8
+ * the pre-gh#929 monitorClause. Built by both launch call sites
9
+ * (claude.ts + assimilate-cmd.ts) and passed to `buildAgentKickoffPrompt` as
10
+ * `monitorClause`. Codex wakes via the app-server (no tail-Monitor / `/loop`
11
+ * to arm) → empty; no active cube (no inboxPath) → empty.
12
+ */
13
+ export function buildKickoffWakePathClause(
14
+ agentKind: AgentKind,
15
+ inboxPath: string | null,
16
+ monitorStateRoot?: string | null
17
+ ): string {
18
+ if (agentKind === 'claude' && inboxPath) {
19
+ return (
20
+ wakePathArming('claude', inboxPath, monitorStateRoot) +
21
+ '\nKeep this Monitor armed for the whole session — NEVER TaskStop it (the generic ' +
22
+ '/loop "TaskStop any Monitor you armed" step does NOT apply to the cube inbox Monitor; ' +
23
+ 'it targets throwaway loop-scratch watches only). The sole exception is a confirmed ' +
24
+ 'terminal eviction (410 DRONE_EVICTED). '
25
+ );
26
+ }
27
+ return '';
28
+ }
29
+
30
+ export interface CodexWakeTargetDeps {
31
+ setCodexWakeTarget: (
32
+ cubeId: string,
33
+ droneId: string,
34
+ target: { threadId: string; socketPath: string }
35
+ ) => Promise<void>;
36
+ findLoadedCodexThread: (options: {
37
+ socketPath: string;
38
+ cwd: string;
39
+ previewIncludes: string;
40
+ updatedAfter: number;
41
+ }) => Promise<string | null>;
42
+ }
43
+
44
+ export function buildAgentKickoffPrompt(options: {
45
+ cli: BorgCli;
46
+ codexWakeNonce: string | null;
47
+ monitorClause: string;
48
+ codexWakePathClause?: string;
49
+ }): string {
50
+ // gh#929: compacted to the load-bearing launch essentials (lean/explicit/
51
+ // imperative, #914 treatment). STRIPPED: the read-log-triage paragraph (the
52
+ // playbook owns it post-#914) + the role-specific anti-passive-Standing
53
+ // clause (Coordinator/Queen-only; belongs in role-text, not injected for
54
+ // ALL). KEPT: core call + MCP-disconnect recovery + the wake-path arming
55
+ // (claude via the shared monitorClause = buildKickoffWakePathClause; codex
56
+ // via codexWakePathClause) + the claude/codex/opencode cli-branching.
57
+ const codexNonceClause = options.codexWakeNonce
58
+ ? `Wake target nonce: ${options.codexWakeNonce}. `
59
+ : '';
60
+ const codexWakePathClause =
61
+ options.codexWakePathClause ??
62
+ `Codex Borg wakeups use remote-control when available; if no wake arrives, run borg_regen manually when returning to the session.`;
63
+ const opencodeWakePathClause = `OpenCode wakes: the inbox Monitor is not available yet for OpenCode; check activity by calling borg_read-log periodically.`;
64
+ const loopOrEmpty = options.cli === 'claude' ? '/loop ' : '';
65
+ const wakeClause = options.cli === 'claude'
66
+ ? options.monitorClause
67
+ : options.cli === 'codex'
68
+ ? codexWakePathClause
69
+ : opencodeWakePathClause;
70
+ return (
71
+ `${loopOrEmpty}Call borg_regen and follow the playbook in its response. ` +
72
+ codexNonceClause +
73
+ `Note: at session start the borg MCP server is still spinning up in ` +
74
+ `parallel — if a system reminder claims "MCP server disconnected" or ` +
75
+ `the borg tools are not yet registered, do NOT bail. Recover via ` +
76
+ `\`ToolSearch({query: "select:mcp__borg__borg_regen,mcp__borg__borg_log,Monitor", max_results: 3})\` ` +
77
+ `to load the bootstrap tools in one call, then call borg_regen. ` +
78
+ `The server typically becomes available within a few seconds. ` +
79
+ wakeClause
80
+ );
81
+ }
82
+
83
+ export function socketPathFromRemoteArgs(args: string[]): string | null {
84
+ const index = args.indexOf('--remote');
85
+ if (index < 0) return null;
86
+ const value = args[index + 1];
87
+ if (!value?.startsWith('unix://')) return null;
88
+ return value.slice('unix://'.length);
89
+ }
90
+
91
+ export function threadIdFromPassthroughArgs(args: string[]): string | null {
92
+ if (args[0] === 'resume' && args[1] && !args[1].startsWith('-')) return args[1];
93
+ const resumeIndex = args.indexOf('--resume');
94
+ if (resumeIndex >= 0 && args[resumeIndex + 1]) return args[resumeIndex + 1];
95
+ return null;
96
+ }
97
+
98
+ export async function recordCodexWakeTarget(options: {
99
+ deps: CodexWakeTargetDeps;
100
+ cubeId: string;
101
+ droneId: string;
102
+ socketPath: string;
103
+ cwd: string;
104
+ previewNeedle: string;
105
+ launchedAtSeconds: number;
106
+ passthroughArgs?: string[];
107
+ }): Promise<void> {
108
+ try {
109
+ const explicitThreadId = options.passthroughArgs
110
+ ? threadIdFromPassthroughArgs(options.passthroughArgs)
111
+ : null;
112
+ if (explicitThreadId) {
113
+ await options.deps.setCodexWakeTarget(options.cubeId, options.droneId, {
114
+ threadId: explicitThreadId,
115
+ socketPath: options.socketPath,
116
+ });
117
+ return;
118
+ }
119
+
120
+ const deadline = Date.now() + 15_000;
121
+ while (Date.now() < deadline) {
122
+ const threadId = await options.deps.findLoadedCodexThread({
123
+ socketPath: options.socketPath,
124
+ cwd: options.cwd,
125
+ previewIncludes: options.previewNeedle,
126
+ updatedAfter: options.launchedAtSeconds - 5,
127
+ });
128
+ if (threadId) {
129
+ await options.deps.setCodexWakeTarget(options.cubeId, options.droneId, {
130
+ threadId,
131
+ socketPath: options.socketPath,
132
+ });
133
+ return;
134
+ }
135
+ await new Promise((resolve) => setTimeout(resolve, 500));
136
+ }
137
+ } catch {
138
+ // Best-effort mapping: launch still succeeds and manual regen remains available.
139
+ }
140
+ }