borgmcp 1.1.15 → 2.0.2

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