agentbox-app 0.1.12 → 0.1.16

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 (370) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,234 @@
1
+ // Finder launches inherit launchd's PATH rather than the user's shell tools.
2
+ // Read only named variables: importing a profile's whole environment would
3
+ // import keys and login overrides too. PATH, the two folders below, and the
4
+ // network and locale names under NETWORK and LOCALE are all that is ever read,
5
+ // and workers still pass through their billing scrub.
6
+ import { execFile } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ // The shell a person actually has. `$SHELL` when that file exists, then zsh,
11
+ // bash, and sh. A machine with no zsh used to spawn `/bin/zsh`: the terminal
12
+ // never came up, and the self-update never built.
13
+ export function loginShell(env = process.env, exists = fs.existsSync) {
14
+ const preferred = env.SHELL;
15
+ if (preferred && exists(preferred)) return preferred;
16
+ for (const candidate of ['/bin/zsh', '/bin/bash', '/bin/sh']) {
17
+ if (exists(candidate)) return candidate;
18
+ }
19
+ return preferred || '/bin/sh';
20
+ }
21
+
22
+ const MARKER = '\0agentbox-path\0';
23
+ function runShell(file, args, options) {
24
+ return new Promise((resolve, reject) => {
25
+ const child = execFile(file, args, options, (error, stdout) => error ? reject(error) : resolve(stdout));
26
+ child.stdin?.end();
27
+ });
28
+ }
29
+
30
+ /**
31
+ * WHICH SHELLS ARE ASKED, IN ORDER, AND WHY THERE IS MORE THAN ONE (2026-10-15).
32
+ *
33
+ * Their own, interactive and login, first: that is the one that reads .zshrc,
34
+ * where nvm and most PATH lines live.
35
+ *
36
+ * Then their own again as a login shell that is NOT interactive, which reads
37
+ * .zprofile and skips .zshrc. Measured with the real /bin/zsh: a .zshrc that
38
+ * runs `[ -z "$TMUX" ] && exec tmux` (tmux exits 1 with no terminal) or
39
+ * `exec fish` (exits 0 having read nothing) ended the shell before our
40
+ * printf, and the app fell back to launchd's four folders, losing the
41
+ * Homebrew line that .zprofile carries. Asking again without -i keeps it. A
42
+ * .zshrc slower than five seconds lands here too.
43
+ *
44
+ * Last, the system's own shell, when theirs is something else: a shell that
45
+ * cannot speak `printf ... "$PATH"` (nushell hands back the words `$PATH`)
46
+ * is a fact about that shell, not about where their tools are. The program
47
+ * finder (main/engine-bin.mjs shellProbes) falls back the same way.
48
+ */
49
+ export function loginShellAttempts(env = process.env, platform = process.platform) {
50
+ const system = platform === 'darwin' ? '/bin/zsh' : '/bin/bash';
51
+ const theirs = env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/sh');
52
+ const out = [[theirs, '-ilc', 5000], [theirs, '-lc', 2500]];
53
+ if (system !== theirs) out.push([system, '-ilc', 2500]);
54
+ return out;
55
+ }
56
+
57
+ /** The first attempt whose output `read` accepts; the first failure otherwise. */
58
+ async function askLoginShell({ env, platform, run, script, read }) {
59
+ let first = null;
60
+ for (const [shell, flags, timeout] of loginShellAttempts(env, platform)) {
61
+ try {
62
+ const stdout = String(await run(shell, [flags, script(shell)], {
63
+ // SIGKILL: a shell's exit hook can outlive SIGTERM (main/engine-bin.mjs).
64
+ env: { ...env }, encoding: 'utf8', timeout, killSignal: 'SIGKILL', maxBuffer: 64 * 1024,
65
+ }));
66
+ return read(stdout);
67
+ } catch (error) {
68
+ first ??= error;
69
+ }
70
+ }
71
+ throw first;
72
+ }
73
+
74
+ const isFish = (shell) => path.basename(shell) === 'fish';
75
+
76
+ /**
77
+ * WHERE THE SHELL SAYS CLAUDE CODE AND CODEX KEEP THEIR SESSIONS (w-e46f2e9cea).
78
+ *
79
+ * An app opened from the Dock, or from a Linux launcher entry, never sees what
80
+ * a shell profile exports, so a CLAUDE_CONFIG_DIR or CODEX_HOME set there was
81
+ * invisible to the import unless the folder happened to be named
82
+ * ~/.claude-<name>. These two names and nothing else are read, the same way
83
+ * PATH is above, and they are handed to the session finder rather than put
84
+ * into the app's environment: CLAUDE_CONFIG_DIR there would change which
85
+ * account every worker runs on.
86
+ *
87
+ * Only an absolute path is kept. A quoted `~` the shell did not expand is
88
+ * expanded here; anything else, or no answer at all, is left out. */
89
+ const DIRS_MARKER = '\0agentbox-dirs\0';
90
+ const DIRS = ['CLAUDE_CONFIG_DIR', 'CODEX_HOME'];
91
+
92
+ /**
93
+ * HOW THIS MACHINE REACHES THE NETWORK, WHEN ONLY THE SHELL PROFILE SAYS (2026-10-15).
94
+ *
95
+ * Behind a company proxy, people export HTTPS_PROXY and the company's
96
+ * certificate (NODE_EXTRA_CA_CERTS) in .zshrc, which is where Claude Code's
97
+ * own error tells them to. A Dock launch never sees it. Measured with the real
98
+ * Claude Code 2.1.301 against a stub model behind a CONNECT proxy and a
99
+ * self-signed certificate: with both variables it answered in 240 ms; with no
100
+ * proxy variable it retried for minutes (10 retries, about three) and failed;
101
+ * with the proxy and no certificate it died in 371 ms on "Self-signed
102
+ * certificate detected". So a worker started by the app failed on exactly the
103
+ * machine where the person's terminal worked.
104
+ *
105
+ * None of these is a key or a login, so the billing scrub has nothing to say
106
+ * about them, and they go into the app's own environment where every worker,
107
+ * terminal and git call inherits them. A name the launch environment already
108
+ * has is left alone, and the proxies are one setting: if any proxy name is
109
+ * already set, none is added.
110
+ */
111
+ const PROXIES = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy'];
112
+ const CERTIFICATES = ['NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE'];
113
+
114
+ /**
115
+ * THE LANGUAGE SETTING A DOCK LAUNCH DOES NOT HAVE (2026-10-15).
116
+ *
117
+ * launchd gives an app no LANG and no LC_*: read off a running Dock-launched
118
+ * Electron app with `ps eww`, and off a bare Electron started with launchd's
119
+ * environment (process.env.LANG undefined, and none in its children).
120
+ * Terminal.app sets LANG itself, so the person's own commands always had one
121
+ * and their agents never did. Measured on this Mac with LANG unset: system
122
+ * Ruby reads files as US-ASCII (what CocoaPods and fastlane refuse to run
123
+ * under), `sed 's/./X/g'` turns "café" into five X's, `wc -m` counts five.
124
+ *
125
+ * So the shell's own, when it has one. On macOS 26 that is usually Apple's:
126
+ * /etc/zprofile exports LANG=C.UTF-8 when nothing else has, so a zsh login
127
+ * answers C.UTF-8 (measured on this Mac), which fixes both of the above and
128
+ * keeps every tool's messages in the English the app reads. Otherwise, on a
129
+ * Mac only, en_US.UTF-8, which exists on every macOS and is what Terminal
130
+ * sets for most people. Linux sessions set their own.
131
+ */
132
+ const LOCALE = ['LANG', 'LC_ALL', 'LC_CTYPE'];
133
+ const MAC_LOCALE = 'en_US.UTF-8';
134
+
135
+ const SETTINGS = [...DIRS, ...PROXIES, ...CERTIFICATES, ...LOCALE];
136
+
137
+ /** Everything above from one login shell, as { dirs, network, locale }. */
138
+ export async function readShellSettings({ env = process.env, platform = process.platform, run = runShell } = {}) {
139
+ const none = { dirs: {}, network: {}, locale: {} };
140
+ if (platform !== 'darwin' && platform !== 'linux') return none;
141
+ const script = () => `printf '\\0agentbox-dirs\\0${'%s\\0'.repeat(SETTINGS.length)}' ${SETTINGS.map((n) => `"$${n}"`).join(' ')}`;
142
+ const read = (stdout) => {
143
+ const start = stdout.lastIndexOf(DIRS_MARKER);
144
+ if (start < 0) throw Error('Your shell did not answer.');
145
+ return stdout.slice(start + DIRS_MARKER.length).split('\0');
146
+ };
147
+ let values;
148
+ try {
149
+ values = await askLoginShell({ env, platform, run, script, read });
150
+ } catch {
151
+ return none;
152
+ }
153
+ const raw = Object.fromEntries(SETTINGS.map((name, i) => [name, String(values[i] ?? '').trim()]));
154
+ const single = (v) => v && !/[\r\n]/.test(v);
155
+ const folder = (v) => {
156
+ if (!single(v)) return null;
157
+ if (v === '~' || v.startsWith('~/')) {
158
+ if (!env.HOME) return null;
159
+ v = path.join(env.HOME, v.slice(1));
160
+ }
161
+ return path.isAbsolute(v) ? v : null;
162
+ };
163
+ const out = { dirs: {}, network: {}, locale: {} };
164
+ for (const name of DIRS) if (folder(raw[name])) out.dirs[name] = folder(raw[name]);
165
+ for (const name of PROXIES) if (single(raw[name])) out.network[name] = raw[name];
166
+ for (const name of CERTIFICATES) if (folder(raw[name])) out.network[name] = folder(raw[name]);
167
+ for (const name of LOCALE) if (single(raw[name]) && /^[A-Za-z0-9_.@-]+$/.test(raw[name])) out.locale[name] = raw[name];
168
+ return out;
169
+ }
170
+
171
+ export async function readShellDataDirs(opts = {}) {
172
+ return (await readShellSettings(opts)).dirs;
173
+ }
174
+
175
+ /** What to add to a launch environment, given what the shell said. Never overwrites. */
176
+ export function launchAdditions({ network = {}, locale = {} } = {}, env = process.env, platform = process.platform) {
177
+ const set = (name) => env[name] !== undefined && env[name] !== '';
178
+ const add = {};
179
+ if (!PROXIES.some(set)) for (const name of PROXIES) if (network[name]) add[name] = network[name];
180
+ for (const name of CERTIFICATES) if (network[name] && !set(name)) add[name] = network[name];
181
+ if (!LOCALE.some(set)) {
182
+ for (const name of LOCALE) if (locale[name]) add[name] = locale[name];
183
+ if (!LOCALE.some((n) => add[n]) && platform === 'darwin') add.LANG = MAC_LOCALE;
184
+ }
185
+ return add;
186
+ }
187
+
188
+ /**
189
+ * What the shell answered, kept for the life of the app. Empty until it
190
+ * answers, which is long before setup reaches its last card.
191
+ *
192
+ * AND THE NETWORK AND LANGUAGE SETTINGS ARE PUT INTO `into` (the app's own
193
+ * environment) here, because this is the one shell read that both the desktop
194
+ * (main/main.mjs) and the browser copy (bin/agentbox.mjs) wait for before the
195
+ * first worker starts. */
196
+ let shellDataEnv = {};
197
+ export const shellDataDirsNow = () => shellDataEnv;
198
+ export function learnShellDataDirs({ into = process.env, ...opts } = {}) {
199
+ return readShellSettings(opts).then((found) => {
200
+ shellDataEnv = found.dirs;
201
+ Object.assign(into, launchAdditions(found, opts.env ?? process.env, opts.platform ?? process.platform));
202
+ return found.dirs;
203
+ });
204
+ }
205
+
206
+ export async function recoverShellPath({ env = process.env, platform = process.platform, run = runShell } = {}) {
207
+ const original = env.PATH || '';
208
+ // LINUX TOO (2026-10-15). A launcher entry starts the app with the desktop
209
+ // session's environment, and that session read ~/.profile without -i, so
210
+ // the nvm lines its installer writes into ~/.bashrc (behind Ubuntu's "if not
211
+ // running interactively, return") never reached it. readShellDataDirs above
212
+ // already asked on Linux for the same reason.
213
+ if (platform !== 'darwin' && platform !== 'linux') return { path: original, error: null };
214
+ const script = (shell) => `printf '\\0agentbox-path\\0%s\\0' ${isFish(shell) ? '(string join : $PATH)' : '"$PATH"'}`;
215
+ const read = (stdout) => {
216
+ // NUL framing keeps messages printed by .zshrc out of the returned PATH.
217
+ const start = stdout.lastIndexOf(MARKER);
218
+ const end = start < 0 ? -1 : stdout.indexOf('\0', start + MARKER.length);
219
+ const recovered = end < 0 ? '' : stdout.slice(start + MARKER.length, end);
220
+ if (!recovered || /[\r\n]/.test(recovered)) throw Error('Your shell did not return its command search path.');
221
+ // Only absolute folders. A shell that does not expand "$PATH" hands the
222
+ // words back, and a relative entry is a folder that moves with every cwd.
223
+ const entries = recovered.split(':').filter((p) => p.startsWith('/'));
224
+ if (!entries.length) throw Error('Your shell did not return its command search path.');
225
+ return entries;
226
+ };
227
+ try {
228
+ const recovered = await askLoginShell({ env, platform, run, script, read });
229
+ const entries = [...new Set([...recovered, ...original.split(':')].filter(Boolean))];
230
+ return { path: entries.join(':'), error: null };
231
+ } catch (error) {
232
+ return { path: original, error: String(error?.message || error) };
233
+ }
234
+ }
@@ -0,0 +1,119 @@
1
+ // What the other side of a merge conflict was for, so the thread that hit it
2
+ // can resolve it without asking the person. Read-only git: nothing here
3
+ // touches a working tree.
4
+
5
+ import { execFile } from 'node:child_process';
6
+
7
+ const GIT_MS = 10_000;
8
+ const MAX_COMMITS = 8;
9
+ const SUMMARY_CHARS = 240;
10
+
11
+ export const KEEP_BOTH = 'Resolve it keeping both changes where they fit. Only ask the person if the two changes want opposite things, and then say which you kept and why.';
12
+
13
+ const CONFLICT_LINE = /^CONFLICT \([^)]*\): .*$/m;
14
+ // Main's own merges from `npm run ship`: "Merge origin/main into agentbox/<id>".
15
+ const SHIPPED_FROM = /\binto agentbox\/([A-Za-z0-9_-]+)\s*$/;
16
+
17
+ export const isMergeConflict = (output) => CONFLICT_LINE.test(String(output ?? ''));
18
+
19
+ /** The files the script's own output says conflicted. */
20
+ export function conflictedFilesIn(output) {
21
+ const files = new Set();
22
+ for (const line of String(output ?? '').split('\n')) {
23
+ const m = line.match(/^CONFLICT \([^)]*\): (?:Merge conflict in (.+)|(\S+) deleted in )/);
24
+ const file = m && (m[1] ?? m[2]);
25
+ if (file) files.add(file.trim());
26
+ }
27
+ return [...files];
28
+ }
29
+
30
+ function git(cwd, args) {
31
+ return new Promise((resolve) => {
32
+ execFile('git', ['-C', cwd, ...args], { encoding: 'utf8', timeout: GIT_MS, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
33
+ resolve({ ok: !err, code: err ? (typeof err.code === 'number' ? err.code : -1) : 0, out: String(stdout ?? '') });
34
+ });
35
+ });
36
+ }
37
+
38
+ /** Files that would conflict merging `upstream` into `head`, or null if git cannot say. */
39
+ export async function filesThatConflict(cwd, head, upstream) {
40
+ const r = await git(cwd, ['merge-tree', '--write-tree', '--name-only', '--no-messages', head, upstream]);
41
+ if (r.code !== 0 && r.code !== 1) return null;
42
+ return r.out.split('\n').slice(1).map((l) => l.trim()).filter(Boolean);
43
+ }
44
+
45
+ // The thread whose ship brought `sha` onto main: the oldest of main's ship
46
+ // merges after it where `sha` came in on the branch side, not main's.
47
+ async function threadThatShipped(cwd, sha, upstream) {
48
+ const r = await git(cwd, ['log', '--merges', '--ancestry-path', '--reverse', '--format=%H %P%x09%s', `${sha}..${upstream}`]);
49
+ const merges = r.out.split('\n').filter(Boolean).slice(0, 3);
50
+ for (const line of merges) {
51
+ const [shas, subject = ''] = line.split('\t');
52
+ const id = subject.match(SHIPPED_FROM)?.[1];
53
+ const mainSide = shas.split(' ')[2];
54
+ if (!id || !mainSide) continue;
55
+ const onMainAlready = await git(cwd, ['merge-base', '--is-ancestor', sha, mainSide]);
56
+ if (onMainAlready.code === 1) return id;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ const oneLine = (text) => String(text ?? '').replace(/\s+/g, ' ').trim();
62
+ const clip = (text, n) => (text.length > n ? `${text.slice(0, n - 1).trimEnd()}…` : text);
63
+
64
+ // The conflicting files, then what changed them on `upstream`, by thread.
65
+ async function otherSide({ cwd, files, upstream, readThread }) {
66
+ const lines = [`Files: ${files.map((f) => `\`${f}\``).join(', ') || 'see the output below'}.`];
67
+ const since = cwd ? (await git(cwd, ['merge-base', 'HEAD', upstream])).out.trim() : '';
68
+ const log = since && files.length
69
+ ? await git(cwd, ['log', '--no-merges', `-n${MAX_COMMITS}`, '--format=%h %H%x09%s', `${since}..${upstream}`, '--', ...files])
70
+ : { out: '' };
71
+ const byThread = new Map();
72
+ for (const entry of log.out.split('\n').filter(Boolean)) {
73
+ const [shas, subject = ''] = entry.split('\t');
74
+ const [short, full] = shas.split(' ');
75
+ const id = await threadThatShipped(cwd, full, upstream);
76
+ if (!byThread.has(id)) byThread.set(id, []);
77
+ byThread.get(id).push(`${short} ${oneLine(subject)}`);
78
+ }
79
+
80
+ if (!byThread.size) return lines;
81
+ lines.push('', `What changed them on ${upstream} since this branch began:`);
82
+ for (const [id, commits] of [...byThread].sort(([a], [b]) => (a === null) - (b === null))) {
83
+ if (!id) {
84
+ lines.push(`- From no thread: ${commits.join('; ')}`);
85
+ continue;
86
+ }
87
+ let thread = null;
88
+ try { thread = readThread(id); } catch {}
89
+ const title = oneLine(thread?.title);
90
+ const about = clip(oneLine(thread?.progress || thread?.problem), SUMMARY_CHARS);
91
+ lines.push(`- Thread ${title ? `"${title}" ` : ''}(${id})${about ? `: ${about}` : ''}`, ` Commits: ${commits.join('; ')}`);
92
+ }
93
+ return lines;
94
+ }
95
+
96
+ /**
97
+ * The brief for a ship that failed, or null when it was not a merge conflict.
98
+ *
99
+ * @param {{ cwd: string, output: string, upstream?: string, readThread?: (id: string) => object|null }} opts
100
+ */
101
+ export async function conflictBrief({ cwd, output, upstream = 'origin/main', readThread = () => null }) {
102
+ if (!isMergeConflict(output)) return null;
103
+ const fromGit = cwd ? await filesThatConflict(cwd, 'HEAD', upstream) : null;
104
+ const files = fromGit?.length ? fromGit : conflictedFilesIn(output);
105
+ return ['The other side of the conflict:', '', ...await otherSide({ cwd, files, upstream, readThread }), '', KEEP_BOTH].join('\n');
106
+ }
107
+
108
+ /** For a branch that has not shipped yet: null unless `upstream` now conflicts with it. */
109
+ export async function earlyConflictNote({ cwd, upstream = 'origin/main', readThread = () => null }) {
110
+ const files = await filesThatConflict(cwd, 'HEAD', upstream);
111
+ if (!files?.length) return null;
112
+ return [
113
+ `${upstream} moved while this thread waited, and this branch no longer merges cleanly with it. Merge ${upstream} into it before you mark it ready to ship.`,
114
+ '',
115
+ ...await otherSide({ cwd, files, upstream, readThread }),
116
+ '',
117
+ KEEP_BOTH,
118
+ ].join('\n');
119
+ }
@@ -22,10 +22,12 @@
22
22
  // cannot change the code that ships it. With no `ship.json`, nothing here does
23
23
  // anything at all.
24
24
 
25
- import { spawn } from 'node:child_process';
25
+ import { spawn, execFile } from 'node:child_process';
26
26
  import fs from 'node:fs';
27
27
  import path from 'node:path';
28
28
  import { Name } from '../shared/product-name.mjs';
29
+ import { SHIP_FAILED } from '../shared/not-shipped.mjs';
30
+ import { conflictBrief, earlyConflictNote } from './ship-conflict-brief.mjs';
29
31
 
30
32
  export const SHIP_LABEL = 'ship';
31
33
  const SHIP_MS = 20 * 60 * 1000;
@@ -77,15 +79,22 @@ export const wantsShipping = (item) => labelsOf(item).includes(SHIP_LABEL);
77
79
  export const withoutShipLabel = (item) => labelsOf(item).filter((l) => l !== SHIP_LABEL);
78
80
 
79
81
  /**
80
- * The next task to ship, oldest ask first, or null.
82
+ * Every task this queue still owes a ship, whether or not a session is on it.
81
83
  *
82
- * Only a row of a project that ships, marked `ship`, with no session on it
83
- * (an agent still working may yet commit), and with a task folder to ship from.
84
+ * A row of a project that ships, marked `ship`, not yet handled for that
85
+ * marking, and with a task folder to ship from. THE WINDOW READS THIS TOO
86
+ * (w-0c1ba766eb): a row on this list is waiting on the app, not on a person,
87
+ * so it is In progress and not Needs you until the queue has dealt with it.
88
+ *
89
+ * A CLOSED ROW STILL SHIPS, because a close is not a cancel. The window
90
+ * leaves closed rows out (`waitingIds`), so one stays in Done while it ships.
84
91
  */
85
- export function nextToShip(items, products, settings, { isLive = () => false, folderFor, handled = {} }) {
92
+ export const owedAShip = (item, handled = {}) => wantsShipping(item) && !(markedAt(item) <= (handled[keyOf(item)] ?? -1));
93
+
94
+ export function waitingToShip(items, products, settings, { folderFor, handled = {} }) {
86
95
  const bySlug = new Map(products.map((p) => [p.slug, p]));
87
- const ready = items
88
- .filter((i) => wantsShipping(i) && i.status !== 'done' && !isLive(i) && !(markedAt(i) <= (handled[keyOf(i)] ?? -1)))
96
+ return items
97
+ .filter((i) => owedAShip(i, handled))
89
98
  .map((item) => {
90
99
  const product = bySlug.get(item.product);
91
100
  const script = product ? shipScriptFor(product, settings) : null;
@@ -93,7 +102,27 @@ export function nextToShip(items, products, settings, { isLive = () => false, fo
93
102
  return script && cwd && fs.existsSync(cwd) ? { item, product, script, cwd } : null;
94
103
  })
95
104
  .filter(Boolean);
96
- ready.sort((a, b) => markedAt(a.item) - markedAt(b.item));
105
+ }
106
+
107
+ /**
108
+ * The next task to ship, the most important first, or null.
109
+ *
110
+ * A session the row's own agent is still finishing holds its ship: its commits
111
+ * may not all be in. A session that started AFTER the label does not, because
112
+ * that is a reply, and a reply to a thread already waiting used to hold it for
113
+ * as long as the new session ran (2026-10-19). `liveSince` answering null
114
+ * means nobody can say when the live work started — a cloud run whose folder
115
+ * has not come home — and that waits, as it always did.
116
+ *
117
+ * Priority before age, the way `shared/work-items.mjs` orders the queue a
118
+ * puller sees: a thread the person is waiting on queued behind every older one.
119
+ */
120
+ export function nextToShip(items, products, settings, {
121
+ isLive = () => false, liveSince = () => null, folderFor, handled = {},
122
+ }) {
123
+ const ready = waitingToShip(items, products, settings, { folderFor, handled })
124
+ .filter(({ item }) => !isLive(item) || liveSince(item) > markedAt(item));
125
+ ready.sort((a, b) => (b.item.priority ?? 0) - (a.item.priority ?? 0) || markedAt(a.item) - markedAt(b.item));
97
126
  return ready[0] ?? null;
98
127
  }
99
128
 
@@ -175,7 +204,7 @@ function diagnosis({ why, failed }) {
175
204
  }
176
205
 
177
206
  /** What the agent is told when its branch did not ship. */
178
- export function failureReply(output) {
207
+ export function failureReply(output, { brief = null } = {}) {
179
208
  const tail = String(output ?? '').trim().slice(-KEEP);
180
209
  const { died, ...rest } = whyItDidNotShip(output);
181
210
  return [
@@ -192,6 +221,7 @@ export function failureReply(output) {
192
221
  'a worker that was starved or timed out is likelier than a bug in your change.',
193
222
  'Run the tests for your change again before you go looking for one.',
194
223
  ] : []),
224
+ ...(brief ? ['', brief] : []),
195
225
  '',
196
226
  'Fix what the script says below in your task folder, commit, then add the label',
197
227
  `\`${SHIP_LABEL}\` to this row again and end your turn. Do not push or touch any`,
@@ -207,16 +237,35 @@ export function failureReply(output) {
207
237
  export function failureNote(output) {
208
238
  const { died, last, ...rest } = whyItDidNotShip(output);
209
239
  const said = died ? `${DIED}. It last said: ${last ?? 'nothing at all'}` : diagnosis(rest);
210
- return `It did not ship, and it went back to its agent: ${said.slice(0, 300)}`;
240
+ return `${SHIP_FAILED}${said.slice(0, 300)}`;
211
241
  }
212
242
 
213
- export function runScript(script, cwd, { timeout = SHIP_MS, env = process.env } = {}) {
243
+ /**
244
+ * The base checkout's hooks, read off the task folder's shared git dir. The
245
+ * ship runs with cwd = the task folder, whose core.hooksPath may be the
246
+ * relative 'scripts/hooks' resolving to the branch's own, worker-edited copy.
247
+ * Pinning the base copy here is what keeps the gate the branch cannot change.
248
+ */
249
+ function baseHooksPath(cwd) {
250
+ return new Promise((resolve) => {
251
+ execFile('git', ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf8', timeout: 10_000 }, (err, stdout) => {
252
+ const common = err ? '' : String(stdout).trim();
253
+ resolve(common ? path.resolve(common, '..', 'scripts', 'hooks') : null);
254
+ });
255
+ });
256
+ }
257
+
258
+ export async function runScript(script, cwd, { timeout = SHIP_MS, env = process.env } = {}) {
214
259
  const node = env.npm_node_execpath && fs.existsSync(env.npm_node_execpath) ? env.npm_node_execpath : 'node';
260
+ const hooks = await baseHooksPath(cwd);
261
+ const baseGate = hooks
262
+ ? { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'core.hooksPath', GIT_CONFIG_VALUE_0: hooks }
263
+ : {};
215
264
  return new Promise((resolve) => {
216
265
  let out = '';
217
266
  let child;
218
267
  try {
219
- child = spawn(node, [script], { cwd, env: { ...env, GIT_TERMINAL_PROMPT: '0' }, stdio: ['ignore', 'pipe', 'pipe'] });
268
+ child = spawn(node, [script], { cwd, env: { ...env, GIT_TERMINAL_PROMPT: '0', ...baseGate }, stdio: ['ignore', 'pipe', 'pipe'] });
220
269
  } catch (e) {
221
270
  resolve({ code: 1, out: String(e?.message ?? e) });
222
271
  return;
@@ -235,41 +284,106 @@ export function runScript(script, cwd, { timeout = SHIP_MS, env = process.env }
235
284
  * supervisor's own tick and returns at once; the run happens beside it.
236
285
  */
237
286
  export class ShipQueue {
238
- constructor({ store, userDir, folderFor, isLive, handBack = () => {}, run = runScript, afterShip = () => {}, log = console }) {
239
- Object.assign(this, { store, userDir, folderFor, isLive, handBack, run, afterShip, log });
287
+ constructor({ store, userDir, folderFor, isLive, liveSince = () => null, handBack = () => {}, run = runScript, afterShip = () => {}, log = console }) {
288
+ Object.assign(this, { store, userDir, folderFor, isLive, liveSince, handBack, run, afterShip, log });
240
289
  this.busy = null;
290
+ this.earlyConflicts = new Map();
241
291
  }
242
292
 
243
293
  tick(items, products) {
244
294
  if (this.busy) return null;
245
295
  const settings = readShipSettings(this.userDir);
246
296
  if (!Object.keys(settings).length) return null;
247
- const next = nextToShip(items, products, settings, { isLive: this.isLive, folderFor: this.folderFor, handled: readHandled(this.userDir) });
297
+ const next = nextToShip(items, products, settings, {
298
+ isLive: this.isLive, liveSince: this.liveSince, folderFor: this.folderFor, handled: readHandled(this.userDir),
299
+ });
248
300
  if (!next) return null;
249
301
  // Marked handled only once the run has ENDED, pass or fail. Marked before
250
302
  // it, a run cut off by an app restart was never tried again
251
303
  // (w-f37a34def6, 2026-10-04). Within one app, `busy` already stops a
252
304
  // second start; across a restart, trying again is the point.
253
305
  const done = () => writeHandled(this.userDir, { ...readHandled(this.userDir), [keyOf(next.item)]: markedAt(next.item) });
254
- this.busy = this.ship(next).then(done, done).finally(() => { this.busy = null; });
306
+ this.busy = this.ship(next, products, items).then(done, done).finally(() => { this.busy = null; });
255
307
  return this.busy;
256
308
  }
257
309
 
258
- async ship({ item, product, script, cwd }) {
310
+ /** The ids of the rows still waiting on this queue, for the window. */
311
+ waitingIds(items, products) {
312
+ const settings = readShipSettings(this.userDir);
313
+ if (!Object.keys(settings).length) return [];
314
+ return waitingToShip(items, products, settings, { folderFor: this.folderFor, handled: readHandled(this.userDir) })
315
+ .filter(({ item }) => item.status !== 'done')
316
+ .map(({ item }) => item.id);
317
+ }
318
+
319
+ /** Whether this row's folder must stay for a ship still to come. */
320
+ owes(item, products) {
321
+ const settings = readShipSettings(this.userDir);
322
+ if (!Object.keys(settings).length) return false;
323
+ return waitingToShip([item], products, settings, { folderFor: this.folderFor, handled: readHandled(this.userDir) }).length > 0;
324
+ }
325
+
326
+ // A thread id from a merge on main, read from this project or another one on
327
+ // the same checkout.
328
+ _readThread(id, product, products) {
329
+ const slugs = [product.slug, ...(products ?? []).filter((p) => p.slug !== product.slug && p.repoPath && p.repoPath === product.repoPath).map((p) => p.slug)];
330
+ for (const slug of slugs) {
331
+ const found = this.store.readItem?.(slug, id);
332
+ if (found) return found;
333
+ }
334
+ return null;
335
+ }
336
+
337
+ // After a ship lands, every other open branch of a project that ships is
338
+ // checked against the new main with `git merge-tree`, which reads commits and
339
+ // touches no task folder. A conflict waits here for that thread's next run.
340
+ async _findNewConflicts(shipped, items, products) {
341
+ const settings = readShipSettings(this.userDir);
342
+ const bySlug = new Map((products ?? []).map((p) => [p.slug, p]));
343
+ for (const other of items ?? []) {
344
+ const product = bySlug.get(other.product);
345
+ if (other.id === shipped.id || other.status === 'done' || !product || !settings[product.slug]) continue;
346
+ const cwd = this.folderFor(other, product);
347
+ if (!cwd || !fs.existsSync(cwd)) continue;
348
+ try {
349
+ const note = await earlyConflictNote({ cwd, readThread: (id) => this._readThread(id, product, products) });
350
+ if (note) this.earlyConflicts.set(keyOf(other), note); else this.earlyConflicts.delete(keyOf(other));
351
+ } catch (e) { this.log.warn?.(`zero: could not check ${other.id} against main:`, e.message); }
352
+ }
353
+ }
354
+
355
+ /** What main's last ship means for this thread's branch, once, or null. */
356
+ takeEarlyConflict(item) {
357
+ const note = this.earlyConflicts.get(keyOf(item)) ?? null;
358
+ this.earlyConflicts.delete(keyOf(item));
359
+ return note;
360
+ }
361
+
362
+ async ship({ item, product, script, cwd }, products = [], items = []) {
259
363
  this.log.info?.(`zero: shipping ${item.id} from ${cwd}`);
260
364
  const { code, out } = await this.run(script, cwd);
261
365
  try {
262
366
  // Already on main: there is nothing to fix, so nobody is woken.
263
367
  const already = /There is nothing to ship/.test(out);
264
368
  if (code === 0 || already) {
265
- this.store.shipped(product.slug, item.id, { sha: shippedSha(out), labels: withoutShipLabel(item) });
266
- this.afterShip();
369
+ const sha = shippedSha(out);
370
+ this.store.shipped(product.slug, item.id, { sha, labels: withoutShipLabel(item) });
371
+ // THE COMMIT TRAVELS WITH THE NEWS. What follows a ship wants to know
372
+ // which commit went out: main/main-went-red.mjs waits for that
373
+ // commit's test run on GitHub, because nothing else is still running
374
+ // five minutes after the agent ended.
375
+ this.afterShip({ item, product, sha });
376
+ await this._findNewConflicts(item, items, products);
267
377
  } else {
268
378
  this.store.shipFailed(product.slug, item.id, { note: failureNote(out), labels: withoutShipLabel(item) });
379
+ let brief = null;
380
+ try {
381
+ brief = await conflictBrief({ cwd, output: out, readThread: (id) => this._readThread(id, product, products) });
382
+ } catch (e) { this.log.warn?.(`zero: could not brief the conflict on ${item.id}:`, e.message); }
269
383
  // Straight back to the session that wrote the change. Not through the
270
384
  // reply on the row: that field is hers, and the app's words written
271
385
  // there would be drawn as hers and could not replace hers anyway.
272
- this.handBack(item, failureReply(out));
386
+ this.handBack(item, failureReply(out, { brief }));
273
387
  }
274
388
  } catch (e) {
275
389
  this.log.warn?.(`zero: could not record the ship of ${item.id}:`, e.message);
@@ -1,3 +1,4 @@
1
+ import { harnessFor } from './harnesses.mjs';
1
2
  // THE FILES A LOGIN WRITES, SO THE APP CAN SEE ONE LAND.
2
3
  //
3
4
  // A signed-out account used to sit out a fixed half hour, and nothing but a
@@ -11,7 +12,7 @@
11
12
  // A moved time is a reason to TRY again, not proof; the try is the proof.
12
13
 
13
14
  import fs from 'node:fs';
14
- import path from 'node:path';
15
+
15
16
 
16
17
  /**
17
18
  * Where one account's login is written.
@@ -21,12 +22,7 @@ import path from 'node:path';
21
22
  * - Codex: `auth.json` inside its CODEX_HOME.
22
23
  */
23
24
  export function signInFiles({ engine = 'claude', folder = null, home }) {
24
- if (engine === 'codex') return folder ? [path.join(folder, 'auth.json')] : [];
25
- if (!folder) {
26
- const dir = path.join(home, '.claude');
27
- return [path.join(home, '.claude.json'), path.join(dir, '.claude.json'), path.join(dir, '.credentials.json')];
28
- }
29
- return [path.join(folder, '.claude.json'), path.join(folder, '.credentials.json')];
25
+ return harnessFor(engine).signInFiles({ folder, home });
30
26
  }
31
27
 
32
28
  /** The newest modification time among them, or 0 when none can be read. */