agentbox-app 0.1.11 → 0.1.13

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 (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. package/renderer/dist/assets/index-D6qncgai.js +0 -1168
package/main/settings.mjs CHANGED
@@ -13,18 +13,20 @@
13
13
  // the project they belong to instead of in a text editor.
14
14
 
15
15
  import fs from 'node:fs';
16
+ import { harnessFor } from './harnesses.mjs';
16
17
  import os from 'node:os';
17
18
  import path from 'node:path';
18
19
  import { codexChoiceToOpen, saveConfig } from './config.mjs';
19
- import { resolveClaudeBin, forgetClaudeBin, INSTALL_URL } from './claude-bin.mjs';
20
+ import { forgetClaudeBin, INSTALL_URL } from './claude-bin.mjs';
20
21
  import { AGENT_MODES } from '../shared/agents.mjs';
21
22
  import { analyticsKey, diagnosticsOn } from './analytics.mjs';
22
23
  import { CLAUDE_PERMISSION_MODES } from '../shared/work-items.mjs';
23
- import { accountSentence, engineTroubleNote } from '../shared/spawn-trouble.mjs';
24
+ import { accountSentence, engineTroubleNote, refineCause } from '../shared/spawn-trouble.mjs';
24
25
  import { readPlan } from './claude-plan.mjs';
25
26
  import { effectiveProfiles } from './account-discovery.mjs';
26
27
  import { machineSlots, machineNote, autoAgents, MAX_SLOTS } from './machine.mjs';
27
- import { NAME } from '../shared/product-name.mjs';
28
+ import { shortPath } from './agent-files.mjs';
29
+ import { NAME, Name } from '../shared/product-name.mjs';
28
30
  import { autoSlots } from './memory-gate.mjs';
29
31
  import { accountIdentity, claudeLoginCommand, duplicateAccountNote, linkAccountTooling, makeClaudeHome } from './account-tooling.mjs';
30
32
  import { isEngine } from '../shared/engines.mjs';
@@ -32,7 +34,7 @@ import { codexAccount, codexLoginCommand, makeCodexHome } from './codex-account.
32
34
  import { codexDefaultModel, codexModels } from './codex-models.mjs';
33
35
  import { CODEX_DEFAULT_MODE, isCodexMode } from '../shared/codex-modes.mjs';
34
36
  import { claudeModels } from './claude-models.mjs';
35
- import { resolveCodexBin, forgetCodexBin, INSTALL_URL as CODEX_INSTALL_URL } from './codex-bin.mjs';
37
+ import { forgetCodexBin, INSTALL_URL as CODEX_INSTALL_URL } from './codex-bin.mjs';
36
38
 
37
39
  // Her Claude Code sessions in her inbox: all of them, only the ones stopped on
38
40
  // a question, or none. One reader, here, so the settings screen and the
@@ -248,13 +250,18 @@ const lineCount = (text) => (text.trim() ? text.trim().split('\n').length : 0);
248
250
  // Claude Code: the path, whether it is really there, and where to get it when
249
251
  // it is not. One reader, here, so the screen and the config cannot disagree.
250
252
  export function claudeState(config) {
251
- const found = resolveClaudeBin(config.claudeBinConfigured ?? null);
253
+ const found = harnessFor('claude').discover(config.claudeBinConfigured ?? null);
252
254
  return {
253
255
  claudeBin: found.path ?? config.claudeBin,
254
256
  claudeFound: found.found,
255
257
  // Whether the search got an answer at all.
256
258
  claudeCertain: found.certain !== false,
257
259
  claudeInstallUrl: INSTALL_URL,
260
+ // WHETHER SOMEBODY POINTED AT THIS FILE THEMSELVES, which is the one thing
261
+ // the card cannot work out from the path alone. A chosen path is never
262
+ // searched around, so the row that offers the search back has to know
263
+ // (`pointAtEngineBin` below).
264
+ claudeChosen: found.from === 'settings',
258
265
  };
259
266
  }
260
267
 
@@ -310,7 +317,7 @@ export function recheckClaude(config) {
310
317
  * whose unsure state is not checked. Nothing in the app passes it.
311
318
  */
312
319
  export function codexState(config, where = {}) {
313
- const found = resolveCodexBin(config.codexBinConfigured ?? null, where);
320
+ const found = harnessFor('codex').discover(config.codexBinConfigured ?? null, where);
314
321
  return {
315
322
  // Empty rather than a path when there is nothing there, which is the same
316
323
  // asymmetry `config.codexBin` already keeps (main/config.mjs): a configured
@@ -322,6 +329,8 @@ export function codexState(config, where = {}) {
322
329
  // Whether the search got an answer at all.
323
330
  certain: found.certain !== false,
324
331
  url: CODEX_INSTALL_URL,
332
+ // Whether somebody pointed at this file themselves. See `claudeState`.
333
+ chosen: found.from === 'settings',
325
334
  };
326
335
  }
327
336
 
@@ -348,6 +357,87 @@ export function recheckCodex(config) {
348
357
  return state;
349
358
  }
350
359
 
360
+ /* ------------------- POINTING AT IT YOURSELF, AND BACK -------------------- */
361
+ /**
362
+ * THE WAY OUT OF A SEARCH THAT MISSED, AND IT IS NOT A RELEASE (w-ce8c1a9275).
363
+ *
364
+ * The note this came from: "It has a really hard time finding the install on the
365
+ * person's computer, and we keep having bug reports and then patching that
366
+ * specific case." Asking the login shell first (main/engine-bin.mjs) covers
367
+ * every installer a person's own PATH knows about, which is most of them. It
368
+ * cannot cover a wrapper script somewhere nobody would guess, a tool on a
369
+ * volume, or a machine whose login shell will not start at all -- and for those,
370
+ * every fix used to be a path WE ship and a release THEY wait for.
371
+ *
372
+ * `claudeBin` in zero.config.json already won over the search and always has.
373
+ * What was missing is that nobody is told about that file and the app gave no
374
+ * way to edit it. These two functions are that door, and the second one matters
375
+ * as much as the first: a wrong choice that could only be undone by hand-editing
376
+ * a file nobody has heard of is not a door, it is a trap.
377
+ *
378
+ * THE REFUSALS ARE MOST OF THE WORK. Each thing refused here was possible by
379
+ * hand before and each one becomes a spawn that fails once per row, naming a
380
+ * file nobody meant to name. The sentence comes back to the screen instead.
381
+ *
382
+ * NO PATH FROM THE WINDOW IS EVER TRUSTED: main/ipc.mjs opens the picker and
383
+ * calls this in the same trip, so the only path that reaches here is one the
384
+ * open panel itself produced. The sibling rule is
385
+ * tests/where-codex-runs-from-is-not-a-file-a-worker-can-write.test.mjs.
386
+ */
387
+ const BIN_KEY = { claude: 'claudeBin', codex: 'codexBin' };
388
+
389
+ /** The sentence, or null when the file really is one we could run. */
390
+ function whyNotAnAgent(chosen) {
391
+ if (typeof chosen !== 'string' || !path.isAbsolute(chosen)) {
392
+ return `${Name} needs the file itself, picked from your computer.`;
393
+ }
394
+ let stat;
395
+ try { stat = fs.statSync(chosen); } catch { stat = null; }
396
+ if (!stat) return `${Name} could not find anything there. It may have moved since.`;
397
+ if (stat.isDirectory()) return 'That is a folder. Open it and pick the file inside.';
398
+ try { fs.accessSync(chosen, fs.constants.X_OK); } catch {
399
+ return 'That file cannot be run, so it is not the one.';
400
+ }
401
+ return null;
402
+ }
403
+
404
+ /**
405
+ * Take a file somebody picked for one coding agent.
406
+ *
407
+ * Written to zero.config.json so it survives a restart AND set on the live
408
+ * config, because every consumer reads `config.*` per call and a setting that
409
+ * waits for a relaunch is the same failure as a fix that is not running. The
410
+ * recheck afterwards is what puts the path the fleet spawns from in step with
411
+ * it, the same way the Check again button does.
412
+ */
413
+ export function pointAtEngineBin(config, engine, chosen) {
414
+ const key = BIN_KEY[engine];
415
+ if (!key) return { ok: false, say: `${Name} does not know a coding agent called that.` };
416
+ const why = whyNotAnAgent(chosen);
417
+ if (why) return { ok: false, say: why };
418
+ if (!config.appDir) return { ok: false, say: `${Name} has nowhere to save that.` };
419
+ saveConfig(config, { [key]: chosen });
420
+ config[`${engine}BinConfigured`] = chosen;
421
+ const state = engine === 'claude' ? recheckClaude(config) : recheckCodex(config);
422
+ return { ok: true, state };
423
+ }
424
+
425
+ /**
426
+ * Give the choice back, so the search is in charge again. The key is REMOVED
427
+ * from the file rather than emptied: `saveConfig` reads an explicit undefined as
428
+ * a deletion, and an empty string left behind would read as a setting that means
429
+ * something else (main/config.mjs only treats a non-empty string as chosen).
430
+ */
431
+ export function searchForEngineBin(config, engine) {
432
+ const key = BIN_KEY[engine];
433
+ if (!key) return { ok: false, say: `${Name} does not know a coding agent called that.` };
434
+ if (!config.appDir) return { ok: false, say: `${Name} has nowhere to save that.` };
435
+ saveConfig(config, { [key]: undefined });
436
+ config[`${engine}BinConfigured`] = null;
437
+ const state = engine === 'claude' ? recheckClaude(config) : recheckCodex(config);
438
+ return { ok: true, state };
439
+ }
440
+
351
441
  export function readSettings({ config, supervisor, store }) {
352
442
  const products = store.listProducts();
353
443
  const status = supervisor.status();
@@ -379,7 +469,11 @@ export function readSettings({ config, supervisor, store }) {
379
469
  // waiting, because a page she chose to open interrupts nobody.
380
470
  const troubles = status.accountTrouble ?? {};
381
471
  const accounts = (profiles.length ? profiles : ['default']).map((profile) => {
382
- const trouble = troubles[profile] ?? null;
472
+ const plan = readPlan(profile);
473
+ // A reason filed before an ended subscription had its own name still reads
474
+ // as an admin's block; the login's file says which it is.
475
+ const filed = troubles[profile] ?? null;
476
+ const trouble = filed ? { ...filed, cause: refineCause(filed.cause, plan) } : null;
383
477
  const resting = (supervisor._profileCooldown?.[profile] ?? 0) >= now;
384
478
  // WHOSE SUBSCRIPTION THIS ROW IS, not merely which folder it lives in.
385
479
  // The page named its rows after folders, so nothing on it could tell a
@@ -421,7 +515,7 @@ export function readSettings({ config, supervisor, store }) {
421
515
  // Read here rather than kept, because Claude Code refreshes its own
422
516
  // profile as it runs and this page is opened by hand a few times a day.
423
517
  // `null` when the file cannot be read, and then no screen says anything.
424
- plan: (() => { const p = readPlan(profile); return p.known ? { label: p.label, max: p.max } : null; })(),
518
+ plan: plan.known ? { label: plan.label, max: plan.max } : null,
425
519
  };
426
520
  });
427
521
 
@@ -449,6 +543,10 @@ export function readSettings({ config, supervisor, store }) {
449
543
  permission: override ? permissionMode(override) : 'workspace',
450
544
  permissionArgs: override ?? null,
451
545
  codexMode: config.projectCodexMode?.[product.slug] ?? 'workspace',
546
+ // THE SUPERVISOR IS ASKED RATHER THAN THE CONFIG FILE, so the page shows
547
+ // what will actually happen: a tie naming a login that has been signed out
548
+ // is ignored when work is handed out, and reads 'any' here too.
549
+ account: supervisor._projectProfile?.(product.slug) ?? 'any',
452
550
  instructions,
453
551
  running: sessions.filter((s) => s.product === product.slug).length,
454
552
  };
@@ -467,7 +565,9 @@ export function readSettings({ config, supervisor, store }) {
467
565
  archivedProjects,
468
566
  workspace: {
469
567
  agentsRunning: !supervisor.paused,
470
- sessionsAtOnce: config.maxConcurrentSessions,
568
+ // The number picked on the menu, for the whole computer, or what is
569
+ // running as the limit now when nothing was picked.
570
+ sessionsAtOnce: typeof config.agentsAtOnce === 'number' ? config.agentsAtOnce : status.capacity,
471
571
  // AUTOMATIC, AND WHAT IT WORKS OUT TO (w-e5225b62ba). `agentsAuto` is who
472
572
  // is deciding; `agentsTotal` is what the whole Mac is running at once,
473
573
  // which is the number the page shows, because the per-account one is
@@ -495,6 +595,7 @@ export function readSettings({ config, supervisor, store }) {
495
595
  machineNote: machineNote(slotsHere, config.maxConcurrentSessions),
496
596
  memoryGate: memoryGateSettings({ config, supervisor }),
497
597
  leftovers: leftoverSettings({ config, supervisor }),
598
+ strayFolders: strayFolderSettings({ supervisor }),
498
599
  capacity: status.capacity,
499
600
  running: sessions.length,
500
601
  model: parseSessionArgs(workspaceArgs).model,
@@ -522,6 +623,9 @@ export function readSettings({ config, supervisor, store }) {
522
623
  // ADHD mode: whether the ADHD rules ride under hers (w-5737fe67cf). Off
523
624
  // unless she turned it on, so only `true` means on.
524
625
  adhdMode: config.adhdMode === true,
626
+ // The corner tag over other apps (w-dafae58a23). On unless she turned it
627
+ // off here, which is the only place it turns off for good.
628
+ cornerTag: config.cornerTag !== false,
525
629
  // How much of her own Claude Code goes in her inbox: all / waiting / off.
526
630
  // All by default, because the machine nobody has opened this page on is
527
631
  // the one with thirteen forgotten sessions on it.
@@ -727,6 +831,24 @@ export function setProjectSetting({ config, supervisor }, { product, key, value
727
831
  saveConfig(config, { projectCodexMode: Object.keys(map).length ? map : undefined });
728
832
  break;
729
833
  }
834
+ // WHICH CLAUDE LOGIN THIS PROJECT'S AGENTS RUN ON. 'any' is the absence of
835
+ // an opinion, like 'workspace' above, and it deletes the entry rather than
836
+ // writing a word: a config file with no `projectAccounts` is the file every
837
+ // Mac had before this existed.
838
+ //
839
+ // AND A NAME THIS MAC CANNOT HONOUR IS REFUSED AT THE DOOR rather than
840
+ // written and ignored later. The supervisor does ignore one (`_projectProfile`),
841
+ // because an account can be signed out after the fact, but a pick made
842
+ // against a list we just read has no excuse to be wrong.
843
+ case 'account': {
844
+ const known = effectiveProfiles(config.authProfiles, { home: config.home });
845
+ if (value !== 'any' && !known.includes(value)) throw new Error(`unknown Claude account: ${value}`);
846
+ const map = { ...(config.projectAccounts ?? {}) };
847
+ if (value === 'any') delete map[product];
848
+ else map[product] = value;
849
+ saveConfig(config, { projectAccounts: Object.keys(map).length ? map : undefined });
850
+ break;
851
+ }
730
852
  default:
731
853
  throw new Error(`unknown project setting: ${key}`);
732
854
  }
@@ -842,7 +964,7 @@ export function memoryGateSettings({ config, supervisor }) {
842
964
  if (on) {
843
965
  let s = null;
844
966
  try { s = supervisor.memoryGateStatus?.() ?? null; } catch {}
845
- if (s?.role === 'standby') now = `Another ${NAME} on this Mac is coordinating.`;
967
+ if (s?.role === 'standby') now = `Another ${NAME} on this computer is coordinating.`;
846
968
  else if (s) {
847
969
  const heavy = (s.running ?? []).filter((r) => r.holdsSlot).length;
848
970
  const waiting = (s.waiting ?? []).length;
@@ -869,6 +991,9 @@ export function memoryGateSettings({ config, supervisor }) {
869
991
  export function memoryReading({ config, supervisor }) {
870
992
  const out = { heavy: null, waiting: null, pressure: null, usedPct: null, asked: null, waited: null, refused: null };
871
993
  if (!config.memoryGate) return out;
994
+ // The supervisor's own hold on new agents (main/start-hold.mjs), which does
995
+ // not depend on which copy of the app is coordinating the command queue.
996
+ try { out.holding = !!supervisor.startHoldStatus?.()?.holding; } catch { out.holding = false; }
872
997
  let s = null;
873
998
  try { s = supervisor.memoryGateStatus?.() ?? null; } catch { return out; }
874
999
  if (!s || s.role === 'standby') return out;
@@ -924,6 +1049,34 @@ export function leftoverSettings({ config, supervisor }) {
924
1049
  return { on, now };
925
1050
  }
926
1051
 
1052
+ /**
1053
+ * COPIES OF A PROJECT THE APP DID NOT MAKE, SAID OUT LOUD (w-330eea6c66).
1054
+ *
1055
+ * MEASURED 2026-10-07: thirteen worktrees of one repository sat outside the
1056
+ * folder the app keeps its own in, about 740 MB, and nothing in the app could
1057
+ * list, show or sweep one. That is the same invisibility that stranded 26 GB on
1058
+ * 2026-09-22, and the half that fixes it for a person is being told.
1059
+ *
1060
+ * NO BUTTON, DELIBERATELY. A folder the app did not make is not the app's to
1061
+ * remove: it is the only copy of whatever is uncommitted inside it, and somebody
1062
+ * made it on purpose. So this is a sentence and nothing else, and it is absent
1063
+ * entirely when there is nothing to say, because a row that is usually quiet
1064
+ * trains the reaction it exists to prevent.
1065
+ */
1066
+ export function strayFolderSettings({ supervisor }, home = os.homedir()) {
1067
+ let list = [];
1068
+ try { list = supervisor?.strayFolders?.() ?? []; } catch { list = []; }
1069
+ if (!list.length) return null;
1070
+ const count = list.length;
1071
+ const one = count === 1;
1072
+ const projects = [...new Set(list.map((w) => w.project).filter(Boolean))];
1073
+ const named = list.slice(0, 3).map((w) => shortPath(w.path, home));
1074
+ const rest = count - named.length;
1075
+ const of = projects.length ? ` of ${projects.slice(0, 2).join(' and ')}${projects.length > 2 ? ' and other projects' : ''}` : '';
1076
+ const now = `${count} ${one ? 'copy' : 'copies'}${of} ${one ? 'was' : 'were'} made by hand outside the folder ${NAME} keeps its own in, and ${one ? 'is' : 'are'} holding disk until you remove ${one ? 'it' : 'them'}: ${named.join(', ')}${rest > 0 ? ` and ${rest} more` : ''}. ${NAME} never removes a folder it did not make.`;
1077
+ return { count, now };
1078
+ }
1079
+
927
1080
  export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
928
1081
  switch (key) {
929
1082
  case 'agentsRunning':
@@ -957,7 +1110,9 @@ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
957
1110
  const n = Math.max(1, Math.min(MAX_SLOTS, Math.round(Number(value) || 1)));
958
1111
  // Setting a number by hand IS taking the decision back, so the page never
959
1112
  // shows a stepper that the supervisor is quietly ignoring.
960
- saveConfig(config, { maxConcurrentSessions: n, agentsAuto: false });
1113
+ // FOR THE WHOLE COMPUTER (w-ec27ff1a51), under its own key, so a number
1114
+ // written per login before 5 Oct can never be read as this choice.
1115
+ saveConfig(config, { agentsAtOnce: n, agentsAuto: false });
961
1116
  // An explicit choice replaces the startup plan suggestion for BOTH
962
1117
  // engines. Leaving this marker set makes Codex ignore the new number
963
1118
  // until loadConfig runs again at restart. It is derived, not persisted.
@@ -1007,6 +1162,11 @@ export function setWorkspaceSetting({ config, supervisor }, { key, value }) {
1007
1162
  case 'adhdMode':
1008
1163
  saveConfig(config, { adhdMode: !!value });
1009
1164
  break;
1165
+ // THE CORNER TAG (w-dafae58a23). Turning it back on also ends any hide, so
1166
+ // the switch does what it says the moment she flips it.
1167
+ case 'cornerTag':
1168
+ saveConfig(config, value ? { cornerTag: true, cornerTagHiddenUntil: 0 } : { cornerTag: false });
1169
+ break;
1010
1170
  // WHICH ACCOUNT HER WORK RUNS ON.
1011
1171
  //
1012
1172
  // ONE KEY HOLDS BOTH ENGINES, because a profile word means different things
@@ -0,0 +1,86 @@
1
+ // Finder launches inherit launchd's PATH rather than the user's shell tools.
2
+ // Read only PATH: importing a profile's whole environment would import keys
3
+ // and login overrides too. Workers still pass through their billing scrub.
4
+ import { execFile } from 'node:child_process';
5
+ import path from 'node:path';
6
+
7
+ const MARKER = '\0agentbox-path\0';
8
+ function runShell(file, args, options) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = execFile(file, args, options, (error, stdout) => error ? reject(error) : resolve(stdout));
11
+ child.stdin?.end();
12
+ });
13
+ }
14
+
15
+ /**
16
+ * WHERE THE SHELL SAYS CLAUDE CODE AND CODEX KEEP THEIR SESSIONS (w-e46f2e9cea).
17
+ *
18
+ * An app opened from the Dock, or from a Linux launcher entry, never sees what
19
+ * a shell profile exports, so a CLAUDE_CONFIG_DIR or CODEX_HOME set there was
20
+ * invisible to the import unless the folder happened to be named
21
+ * ~/.claude-<name>. These two names and nothing else are read, the same way
22
+ * PATH is above, and they are handed to the session finder rather than put
23
+ * into the app's environment: CLAUDE_CONFIG_DIR there would change which
24
+ * account every worker runs on.
25
+ *
26
+ * Only an absolute path is kept. A quoted `~` the shell did not expand is
27
+ * expanded here; anything else, or no answer at all, is left out. */
28
+ const DIRS_MARKER = '\0agentbox-dirs\0';
29
+ export async function readShellDataDirs({ env = process.env, platform = process.platform, run = runShell } = {}) {
30
+ if (platform !== 'darwin' && platform !== 'linux') return {};
31
+ const shell = env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/sh');
32
+ try {
33
+ const stdout = String(await run(shell, ['-ilc', `printf '\\0agentbox-dirs\\0%s\\0%s\\0' "$CLAUDE_CONFIG_DIR" "$CODEX_HOME"`], {
34
+ env: { ...env }, encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024,
35
+ }));
36
+ const start = stdout.lastIndexOf(DIRS_MARKER);
37
+ if (start < 0) return {};
38
+ const [claude = '', codex = ''] = stdout.slice(start + DIRS_MARKER.length).split('\0');
39
+ const out = {};
40
+ const keep = (name, raw) => {
41
+ let v = String(raw ?? '').trim();
42
+ if (!v || /[\r\n]/.test(v)) return;
43
+ if (v === '~' || v.startsWith('~/')) {
44
+ if (!env.HOME) return;
45
+ v = path.join(env.HOME, v.slice(1));
46
+ }
47
+ if (path.isAbsolute(v)) out[name] = v;
48
+ };
49
+ keep('CLAUDE_CONFIG_DIR', claude);
50
+ keep('CODEX_HOME', codex);
51
+ return out;
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+
57
+ /**
58
+ * What the shell answered, kept for the life of the app. Empty until it
59
+ * answers, which is long before setup reaches its last card. */
60
+ let shellDataEnv = {};
61
+ export const shellDataDirsNow = () => shellDataEnv;
62
+ export function learnShellDataDirs(opts) {
63
+ return readShellDataDirs(opts).then((found) => { shellDataEnv = found; return found; });
64
+ }
65
+
66
+ export async function recoverShellPath({ env = process.env, platform = process.platform, run = runShell } = {}) {
67
+ const original = env.PATH || '';
68
+ if (platform !== 'darwin') return { path: original, error: null };
69
+ const shell = env.SHELL || '/bin/zsh';
70
+ const value = path.basename(shell) === 'fish' ? '(string join : $PATH)' : '"$PATH"';
71
+ try {
72
+ // NUL framing keeps messages printed by .zshrc out of the returned PATH.
73
+ const stdout = String(await run(shell, ['-ilc', `printf '\\0agentbox-path\\0%s\\0' ${value}`], {
74
+ env: { ...env }, encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024,
75
+ }));
76
+ const start = stdout.lastIndexOf(MARKER);
77
+ const end = start < 0 ? -1 : stdout.indexOf('\0', start + MARKER.length);
78
+ const recovered = end < 0 ? '' : stdout.slice(start + MARKER.length, end);
79
+ if (!recovered || /[\r\n]/.test(recovered)) throw Error('Your shell did not return its command search path.');
80
+ const entries = [...new Set([...recovered.split(':'), ...original.split(':')].filter(Boolean))];
81
+ if (!entries.length) throw Error('Your shell returned an empty command search path.');
82
+ return { path: entries.join(':'), error: null };
83
+ } catch (error) {
84
+ return { path: original, error: String(error?.message || error) };
85
+ }
86
+ }
@@ -77,15 +77,17 @@ export const wantsShipping = (item) => labelsOf(item).includes(SHIP_LABEL);
77
77
  export const withoutShipLabel = (item) => labelsOf(item).filter((l) => l !== SHIP_LABEL);
78
78
 
79
79
  /**
80
- * The next task to ship, oldest ask first, or null.
80
+ * Every task this queue still owes a ship, whether or not a session is on it.
81
81
  *
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.
82
+ * A row of a project that ships, marked `ship`, not yet handled for that
83
+ * marking, and with a task folder to ship from. THE WINDOW READS THIS TOO
84
+ * (w-0c1ba766eb): a row on this list is waiting on the app, not on a person,
85
+ * so it is In progress and not Needs you until the queue has dealt with it.
84
86
  */
85
- export function nextToShip(items, products, settings, { isLive = () => false, folderFor, handled = {} }) {
87
+ export function waitingToShip(items, products, settings, { folderFor, handled = {} }) {
86
88
  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)))
89
+ return items
90
+ .filter((i) => wantsShipping(i) && i.status !== 'done' && !(markedAt(i) <= (handled[keyOf(i)] ?? -1)))
89
91
  .map((item) => {
90
92
  const product = bySlug.get(item.product);
91
93
  const script = product ? shipScriptFor(product, settings) : null;
@@ -93,6 +95,14 @@ export function nextToShip(items, products, settings, { isLive = () => false, fo
93
95
  return script && cwd && fs.existsSync(cwd) ? { item, product, script, cwd } : null;
94
96
  })
95
97
  .filter(Boolean);
98
+ }
99
+
100
+ /**
101
+ * The next task to ship, oldest ask first, or null: one the queue owes, with
102
+ * no session on it (an agent still working may yet commit).
103
+ */
104
+ export function nextToShip(items, products, settings, { isLive = () => false, folderFor, handled = {} }) {
105
+ const ready = waitingToShip(items, products, settings, { folderFor, handled }).filter(({ item }) => !isLive(item));
96
106
  ready.sort((a, b) => markedAt(a.item) - markedAt(b.item));
97
107
  return ready[0] ?? null;
98
108
  }
@@ -188,7 +198,7 @@ export function failureReply(output) {
188
198
  ...(died ? [
189
199
  '',
190
200
  'Its test run DIED rather than going red: nothing it printed names a failing',
191
- 'test. Look for what killed the run. This Mac runs a dozen agents at once, so',
201
+ 'test. Look for what killed the run. This computer runs a dozen agents at once, so',
192
202
  'a worker that was starved or timed out is likelier than a bug in your change.',
193
203
  'Run the tests for your change again before you go looking for one.',
194
204
  ] : []),
@@ -255,6 +265,14 @@ export class ShipQueue {
255
265
  return this.busy;
256
266
  }
257
267
 
268
+ /** The ids of the rows still waiting on this queue, for the window. */
269
+ waitingIds(items, products) {
270
+ const settings = readShipSettings(this.userDir);
271
+ if (!Object.keys(settings).length) return [];
272
+ return waitingToShip(items, products, settings, { folderFor: this.folderFor, handled: readHandled(this.userDir) })
273
+ .map(({ item }) => item.id);
274
+ }
275
+
258
276
  async ship({ item, product, script, cwd }) {
259
277
  this.log.info?.(`zero: shipping ${item.id} from ${cwd}`);
260
278
  const { code, out } = await this.run(script, cwd);
@@ -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. */
@@ -55,7 +55,13 @@ const SAY = {
55
55
  // on this Mac, and the restart below runs that build. Counted as her changes,
56
56
  // they made every update after the first say "changes of your own" for good
57
57
  // (tests/a-build-does-not-stop-the-next-update.test.mjs).
58
- const isBuildOutput = (file) => /^shared\/[^/]+\.generated\.[cm]?[jt]s$/.test(file);
58
+ //
59
+ // THE LOCKFILE IS NPM'S, TOO (2026-10-07). The restart's `npm install` rewrote
60
+ // package-lock.json (this npm drops its `libc` lines), and the card never came
61
+ // back while main ran 21 commits ahead. An edit to the dependencies themselves
62
+ // is in package.json, which still blocks
63
+ // (tests/a-package-install-does-not-stop-the-next-update.test.mjs).
64
+ const isBuildOutput = (file) => file === 'package-lock.json' || /^shared\/[^/]+\.generated\.[cm]?[jt]s$/.test(file);
59
65
  // The status code is one or two letters and `gitIn` trims the output, which
60
66
  // takes the leading space off the first line, so it is matched, not sliced.
61
67
  const changedPaths = (porcelain) => String(porcelain ?? '').split('\n').filter(Boolean).map((l) => l.replace(/^[ MADRCUT?!]{1,2} /, '').replace(/^"|"$/g, ''));
@@ -128,6 +134,18 @@ export function createSourceUpdater({
128
134
  log = console,
129
135
  onChanged = () => {},
130
136
  npm = defaultNpm(appDir),
137
+ // THE STEP `npm start` DOES AND THIS DID NOT. A from-source run gets its
138
+ // name, icon and bundle id from scripts/brand-electron.mjs, which npm runs
139
+ // before Electron. An update does not go through npm: it builds and calls
140
+ // app.relaunch(), which re-executes the same binary. So the branding had
141
+ // never once run on a self-restart, and a fix to it could sit on disk for
142
+ // days without reaching the bundle (measured 2026-10-09, with the
143
+ // Info.plist four days older than the fix meant to change it).
144
+ // Imported here and not at the top of the file: scripts/ is deliberately not
145
+ // in the downloadable build (`build.files`, and a test holds it there), and
146
+ // a static import would stop that build booting at all. This path only ever
147
+ // runs from source, where the script is on disk.
148
+ brand = async () => (await import('../scripts/brand-electron.mjs')).brandElectron(),
131
149
  relaunch = () => { app?.relaunch(); app?.quit(); },
132
150
  } = {}) {
133
151
  const git = gitIn(appDir);
@@ -251,12 +269,20 @@ export function createSourceUpdater({
251
269
  set({ phase: 'ready', error: `The new packages would not install: ${failureLine(installed.out)}` });
252
270
  return;
253
271
  }
272
+ // What the install rewrote goes back, so the folder is left as main has
273
+ // it and anything else that fast-forwards it cannot trip on the file.
274
+ await git(['checkout', '--', 'package-lock.json']);
254
275
  }
255
276
  const built = await npm(['run', 'build']);
256
277
  if (built.code !== 0) {
257
278
  set({ phase: 'ready', error: `The new code would not build: ${failureLine(built.out)}` });
258
279
  return;
259
280
  }
281
+ // Before the relaunch, never after: the next launch is the one that reads
282
+ // the Info.plist. And never fatally, for the reason brand-electron.mjs
283
+ // opens with about itself — an app that does not come back up is worse
284
+ // than one wearing the wrong icon.
285
+ try { await brand(); } catch (e) { log.warn?.('[source-updater] could not rebrand the bundle:', e?.message ?? e); }
260
286
  log.info?.('[source-updater] built', state.newVersion, '- relaunching');
261
287
  relaunch();
262
288
  }
@@ -0,0 +1,73 @@
1
+ // NO NEW AGENT STARTS WHILE MEMORY IS SHORT (w-ec27ff1a51).
2
+ //
3
+ // 2026-10-11, a 16 GB Mac ran out of application memory with Agentbox at
4
+ // 22 GB and six agents running. The memory check (main/memory-gate.mjs) held
5
+ // tests and builds back, but nothing asked memory before STARTING an agent, so
6
+ // the fleet filled every slot it had into a Mac that was already short.
7
+ //
8
+ // So while the memory switch is on and the Mac reads tight or very short, a new
9
+ // agent waits. That is the whole rule. Everything else here is a way out of it,
10
+ // because the founder's condition was that urgent work still comes first and
11
+ // that nothing sits stuck for long on a misread:
12
+ //
13
+ // - NOTHING RUNNING, ONE STARTS. Whatever is filling memory then is not us,
14
+ // and an app with no agent running at all is broken rather than careful.
15
+ // - AN URGENT ROW, OR ONE SHE ASKED FOR BY NAME, starts while tight and waits
16
+ // only while very short: the command gate's rule for urgent commands.
17
+ // - A SWAP IS NOT A NEW AGENT. A row something was paused for (an Urgent row
18
+ // taking a lesser session's slot, or Run now) always starts, so urgent work
19
+ // still takes over while memory is very short, by replacing, not adding.
20
+ // - A READING THAT IS MISSING, UNREADABLE OR OLDER THAN A MINUTE IS NOT A
21
+ // REASON TO HOLD. The hold acts only on a reading it actually has.
22
+ // - TEN MINUTES HELD, ONE STARTS ANYWAY, and the ten minutes start again from
23
+ // it. A reading stuck on "tight" by mistake costs a slow queue, never a
24
+ // stopped one.
25
+ //
26
+ // Pure, so every one of those is a test (tests/no-new-agent-starts-while-memory-is-short).
27
+
28
+ import { execFileSync } from 'node:child_process';
29
+ import { pressureFrom } from './memory-gate.mjs';
30
+
31
+ /** One reading, synchronously, for the moment before a restart brings the
32
+ * fleet back. Nulls (so no hold) when it cannot be read. */
33
+ export function readMacPressureNow() {
34
+ let level = null;
35
+ let freePct = null;
36
+ try {
37
+ const out = execFileSync('/usr/sbin/sysctl', ['-n', 'kern.memorystatus_vm_pressure_level', 'kern.memorystatus_level'],
38
+ { timeout: 2_000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
39
+ [level, freePct] = out.trim().split(/\s+/).map(Number);
40
+ } catch { /* not a Mac, or sysctl did not answer */ }
41
+ return { level: Number.isFinite(level) ? level : null, freePct: Number.isFinite(freePct) ? freePct : null, at: Date.now() };
42
+ }
43
+
44
+ /** A reading older than this is not acted on. */
45
+ export const STALE_MS = 60_000;
46
+ /** The longest new work waits on memory before one agent starts anyway. */
47
+ export const MAX_HOLD_MS = 10 * 60_000;
48
+
49
+ /**
50
+ * Whether one more agent may start now, and why. `reading` is
51
+ * `{ level, freePct, at }` from `readMacPressure`, stamped when it arrived.
52
+ * `heldSince` is when the current hold began (null when nothing is held);
53
+ * `lastStartAt` is when an agent last started.
54
+ */
55
+ export function startDecision({
56
+ on, reading, now, running,
57
+ urgent = false, asked = false, swap = false,
58
+ lastStartAt = 0, heldSince = null,
59
+ staleMs = STALE_MS, maxHoldMs = MAX_HOLD_MS,
60
+ }) {
61
+ if (!on) return { start: true, why: 'off' };
62
+ if (!(running > 0)) return { start: true, why: 'nothing-running' };
63
+ if (swap) return { start: true, why: 'swap' };
64
+ const at = Number(reading?.at);
65
+ const known = reading && (Number.isFinite(reading.level) || Number.isFinite(reading.freePct));
66
+ if (!known || !Number.isFinite(at) || Math.abs(now - at) > staleMs) return { start: true, why: 'no-reading' };
67
+ const pressure = pressureFrom(reading);
68
+ if (pressure === 'normal') return { start: true, why: 'normal' };
69
+ if (pressure === 'tight' && (urgent || asked)) return { start: true, why: 'urgent' };
70
+ const waitingSince = Math.max(heldSince ?? now, Number(lastStartAt) || 0);
71
+ if (now - waitingSince >= maxHoldMs) return { start: true, why: 'overdue' };
72
+ return { start: false, why: pressure };
73
+ }