agentbox-app 0.1.7 → 0.1.11

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 (244) hide show
  1. package/README.md +209 -55
  2. package/bin/agentbox.mjs +6 -0
  3. package/briefs/message-rules.md +13 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +9 -10
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +114 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/artifact-path.mjs +11 -0
  16. package/main/claude-bin.mjs +31 -5
  17. package/main/claude-input.mjs +45 -3
  18. package/main/claude-plan.mjs +11 -11
  19. package/main/code-change.mjs +3 -3
  20. package/main/codex-account.mjs +1 -1
  21. package/main/codex-bin.mjs +17 -0
  22. package/main/codex-memory-gate.mjs +260 -0
  23. package/main/codex-models.mjs +12 -7
  24. package/main/codex-threads.mjs +46 -23
  25. package/main/codex-usage-file.mjs +7 -7
  26. package/main/codex-usage.mjs +4 -4
  27. package/main/codex-watch.mjs +12 -12
  28. package/main/config.mjs +68 -8
  29. package/main/dev-window.mjs +9 -9
  30. package/main/doc-scheme.mjs +2 -0
  31. package/main/engine-setup.mjs +193 -0
  32. package/main/feedback.mjs +55 -0
  33. package/main/folder-pictures.mjs +48 -0
  34. package/main/frame-scrollbars.mjs +90 -0
  35. package/main/fresh-user.mjs +6 -3
  36. package/main/git-change-offthread.mjs +70 -0
  37. package/main/git-change-thread.mjs +11 -0
  38. package/main/git-change.mjs +1 -1
  39. package/main/img-scheme.mjs +70 -1
  40. package/main/instruction-settings.mjs +92 -54
  41. package/main/ipc.mjs +316 -42
  42. package/main/leftovers.mjs +518 -0
  43. package/main/live-replies.mjs +43 -5
  44. package/main/machine.mjs +47 -0
  45. package/main/main.mjs +119 -15
  46. package/main/memory-gate-history.mjs +254 -0
  47. package/main/memory-gate-server.mjs +461 -0
  48. package/main/memory-gate.mjs +206 -0
  49. package/main/message-priority.mjs +87 -0
  50. package/main/rail-note.mjs +2 -2
  51. package/main/refresh-agent-models.mjs +20 -0
  52. package/main/repeats.mjs +4 -4
  53. package/main/row-label.mjs +114 -51
  54. package/main/serve.mjs +67 -5
  55. package/main/settings.mjs +223 -26
  56. package/main/ship-queue.mjs +279 -0
  57. package/main/sign-in-files.mjs +39 -0
  58. package/main/source-updater.mjs +298 -0
  59. package/main/store/home.mjs +47 -2
  60. package/main/store/work-items.mjs +150 -6
  61. package/main/store.mjs +235 -9
  62. package/main/supervisor.mjs +1444 -406
  63. package/main/task-commands.mjs +8 -10
  64. package/main/task-folders.mjs +74 -13
  65. package/main/task-remote-control.mjs +3 -3
  66. package/main/task-terminals.mjs +2 -1
  67. package/main/team/index.mjs +440 -0
  68. package/main/team/memory-cloud.mjs +247 -0
  69. package/main/team/projects.mjs +162 -0
  70. package/main/team/session.mjs +225 -0
  71. package/main/team/sign-in.mjs +106 -0
  72. package/main/team/supabase-backend.mjs +231 -0
  73. package/main/team/sync.mjs +163 -0
  74. package/main/updater.mjs +15 -5
  75. package/main/write-before-reload.mjs +50 -0
  76. package/mcp/core/page.mjs +9 -11
  77. package/mcp/core/work.mjs +91 -25
  78. package/mcp/tools.mjs +12 -6
  79. package/package.json +21 -9
  80. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  81. package/renderer/dist/assets/index-B-2Az952.css +32 -0
  82. package/renderer/dist/assets/index-D6qncgai.js +1168 -0
  83. package/renderer/dist/index.html +2 -2
  84. package/scripts/memory-gate-hook.sh +59 -0
  85. package/shared/agent-import.mjs +38 -15
  86. package/shared/agents.d.mts +26 -8
  87. package/shared/agents.mjs +100 -108
  88. package/shared/analytics-events.mjs +19 -2
  89. package/shared/answers.mjs +12 -0
  90. package/shared/bridge-map.d.mts +12 -0
  91. package/shared/bridge-map.mjs +37 -1
  92. package/shared/claude-commands.generated.mjs +2 -2
  93. package/shared/claude-models.generated.mjs +4 -4
  94. package/shared/claude-usage.mjs +6 -6
  95. package/shared/codex-import.mjs +7 -7
  96. package/shared/codex-modes.d.mts +50 -0
  97. package/shared/codex-usage.mjs +7 -8
  98. package/shared/demo-world.mjs +5 -5
  99. package/shared/engines.mjs +27 -10
  100. package/shared/feedback.d.mts +14 -0
  101. package/shared/feedback.mjs +44 -0
  102. package/shared/first-run-examples.mjs +4 -4
  103. package/shared/first-run-practice.d.mts +16 -0
  104. package/shared/first-run-practice.mjs +176 -70
  105. package/shared/fresh-user-home.mjs +22 -3
  106. package/shared/instruction-defaults.json +2 -2
  107. package/shared/product-name.mjs +4 -2
  108. package/shared/project-folder-check.mjs +6 -0
  109. package/shared/rank.d.mts +1 -0
  110. package/shared/rank.mjs +22 -0
  111. package/shared/referenced-files.mjs +1 -1
  112. package/shared/schemes.d.mts +11 -0
  113. package/shared/schemes.mjs +1 -1
  114. package/shared/shortcuts.d.mts +41 -0
  115. package/shared/shortcuts.mjs +236 -0
  116. package/shared/spawn-trouble.d.mts +2 -0
  117. package/shared/spawn-trouble.mjs +39 -18
  118. package/shared/team-rules.d.mts +17 -0
  119. package/shared/team-rules.mjs +230 -0
  120. package/shared/team-status.d.mts +11 -0
  121. package/shared/team-status.mjs +90 -0
  122. package/shared/terminal-links.d.mts +2 -0
  123. package/shared/terminal-links.mjs +55 -0
  124. package/shared/terminal-state.d.mts +1 -0
  125. package/shared/terminal-state.mjs +16 -0
  126. package/shared/thread-cards.d.mts +19 -0
  127. package/shared/thread-cards.mjs +254 -0
  128. package/shared/usage.d.mts +1 -1
  129. package/shared/usage.mjs +6 -7
  130. package/shared/work-items.mjs +255 -27
  131. package/shared/work-lines.mjs +393 -16
  132. package/worker-permissions.json +1 -1
  133. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  134. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  135. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  136. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  137. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  138. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  139. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  140. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  141. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  142. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  143. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  144. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  145. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  146. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  147. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  148. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  149. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  150. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  151. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  152. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  153. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  154. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  155. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  156. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  157. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  158. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  159. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  160. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  161. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  162. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  163. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  164. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  165. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  166. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  167. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  168. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  169. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  170. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  171. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  172. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  173. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  174. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  175. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  176. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  177. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  178. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  179. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  180. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  181. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  182. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  183. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  184. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  185. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  186. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  187. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  188. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  189. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  190. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  191. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  192. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  193. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  194. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  195. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  196. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  197. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  198. package/renderer/dist/emulator/data/compression/README.md +0 -11
  199. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  200. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  201. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  202. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  203. package/renderer/dist/emulator/data/cores/README.md +0 -22
  204. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  205. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  206. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  207. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  208. package/renderer/dist/emulator/data/cores/package.json +0 -65
  209. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  210. package/renderer/dist/emulator/data/emulator.css +0 -1614
  211. package/renderer/dist/emulator/data/loader.js +0 -170
  212. package/renderer/dist/emulator/data/localization/README.md +0 -63
  213. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  214. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  215. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  216. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  217. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  218. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  219. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  220. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  221. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  222. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  223. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  224. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  225. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  226. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  227. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  228. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  229. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  230. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  231. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  232. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  233. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  234. package/renderer/dist/emulator/data/src/compression.js +0 -142
  235. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  236. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  237. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  238. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  239. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  240. package/renderer/dist/emulator/data/src/storage.js +0 -125
  241. package/renderer/dist/emulator/data/version.json +0 -5
  242. package/renderer/dist/emulator/host.js +0 -299
  243. package/renderer/dist/emulator/index.html +0 -28
  244. package/renderer/dist/emulator/provenance.json +0 -6
@@ -14,11 +14,11 @@
14
14
  // events, of which `user_message` and
15
15
  // `agent_message` are the two this reads.
16
16
  //
17
- // WHICH THREADS ARE HERS. `thread_source` is `user` for one she typed into,
18
- // `guardian_review` for Codex's own reviewer of a turn, and `subagent` for a
19
- // helper a thread spawned. 247 transcripts were touched in the seven days to
20
- // 09-14; 158 of them were guardians. Without this filter the inbox would offer
21
- // her four rows for every conversation she had.
17
+ // WHICH THREADS ARE THE USER'S. `thread_source` is `user` for one a person typed
18
+ // into, `guardian_review` for Codex's own reviewer of a turn, and `subagent` for
19
+ // a helper a thread spawned. Guardians are the majority of transcripts, so
20
+ // without this filter the inbox would offer several rows for every real
21
+ // conversation.
22
22
  //
23
23
  // There is also `~/.codex/state_5.sqlite`, which has everything above in one
24
24
  // table. It is not read: Electron ships no sqlite, the file is written live
@@ -26,9 +26,12 @@
26
26
  import fs from 'node:fs';
27
27
  import os from 'node:os';
28
28
  import path from 'node:path';
29
- import { isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
29
+ import { RECENT_DAYS, isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
30
+ import { WAS_SLUGS, nameSlug } from '../shared/product-name.mjs';
30
31
 
31
- export const CODEX_RECENT_DAYS = 7;
32
+ export const CODEX_RECENT_DAYS = RECENT_DAYS;
33
+ /** The originator Codex records for a run this app started (CLIENT_INFO.name), now and before. */
34
+ const OUR_ORIGINATORS = new Set([nameSlug, ...WAS_SLUGS]);
32
35
  /** How many day folders the walk opens at most, whatever `days` says. */
33
36
  const MAX_DAY_FOLDERS = 40;
34
37
  /**
@@ -62,22 +65,38 @@ const numericDesc = (dir) => {
62
65
  return names.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
63
66
  };
64
67
 
65
- /** Every transcript in the newest `days` day folders, newest folder first. */
66
- export function listRolloutFiles(codexDir = codexHome(), { days = CODEX_RECENT_DAYS } = {}) {
68
+ /**
69
+ * Every transcript in the newest `days` day folders, newest folder first.
70
+ *
71
+ * AND, WITH `since`, EVERY OLDER ONE WRITTEN TO SINCE THEN. Codex files a
72
+ * conversation under the day it STARTED, so a thread begun two weeks ago and
73
+ * worked in yesterday sits in a folder the newest-`days` walk never opens
74
+ * (w-db6f5e331e). Past those folders only a stat is paid per file, and the
75
+ * walk still stops at MAX_DAY_FOLDERS. */
76
+ export function listRolloutFiles(codexDir = codexHome(), { days = CODEX_RECENT_DAYS, since = null } = {}) {
67
77
  const root = path.join(codexDir, 'sessions');
68
78
  const out = [];
69
79
  let looked = 0;
70
80
  const limit = Math.min(Math.max(1, Number(days) || 1), MAX_DAY_FOLDERS);
81
+ const reach = since == null ? limit : MAX_DAY_FOLDERS;
71
82
  for (const year of numericDesc(root)) {
72
83
  for (const month of numericDesc(path.join(root, year))) {
73
84
  for (const day of numericDesc(path.join(root, year, month))) {
74
- if (looked >= limit) return out;
85
+ if (looked >= reach) return out;
86
+ const older = looked >= limit;
75
87
  looked += 1;
76
88
  const dir = path.join(root, year, month, day);
77
89
  let names = [];
78
90
  try { names = fs.readdirSync(dir); } catch { names = []; }
79
91
  for (const name of names) {
80
- if (/^rollout-.*\.jsonl$/.test(name)) out.push(path.join(dir, name));
92
+ if (!/^rollout-.*\.jsonl$/.test(name)) continue;
93
+ const file = path.join(dir, name);
94
+ if (older) {
95
+ let mtime = 0;
96
+ try { mtime = fs.statSync(file).mtimeMs; } catch { continue; }
97
+ if (mtime < since) continue;
98
+ }
99
+ out.push(file);
81
100
  }
82
101
  }
83
102
  }
@@ -103,17 +122,17 @@ export function cleanPrompt(text) {
103
122
  * WHAT CODEX PUTS IN HER MOUTH. In paginated history mode the user turn is a
104
123
  * `response_item` with several `<recommended_plugins>…`,
105
124
  * `<environment_context>…`, `<user_instructions>…`. Every one of those opens
106
- * with an angle-bracket tag on its first line, and nothing she types does.
107
- * Measured on the thread she screenshotted (2026-09-15): three parts, the
125
+ * with an angle-bracket tag on its first line, and nothing a person types does.
126
+ * Measured on a real thread: three parts, the
108
127
  * first two injected. */
109
128
  export function isInjected(text) {
110
129
  const t = String(text ?? '').trimStart();
111
130
  const first = t.split('\n')[0] ?? '';
112
131
  if (/^<[a-z_]+[^>]*>/i.test(first)) return true;
113
132
  // The folder's AGENTS.md, pasted in as "# AGENTS.md instructions for
114
- // /path" followed by an <INSTRUCTIONS> block. Measured on the thread she
115
- // screenshotted (2026-09-15): it was the second part of her first turn, and
116
- // the first read of it printed it as what she asked.
133
+ // /path" followed by an <INSTRUCTIONS> block. Measured on a real thread: it
134
+ // was the second part of the first turn, and the first read of it printed it
135
+ // as what the user asked.
117
136
  if (/^#\s*AGENTS\.md instructions/i.test(first)) return true;
118
137
  return /^\s*<INSTRUCTIONS>/i.test(t.slice(first.length, first.length + 200));
119
138
  }
@@ -142,9 +161,8 @@ const partsOf = (payload) => (Array.isArray(payload?.content) ? payload.content
142
161
  * lines of type `user_message` / `agent_message` with a `message` string.
143
162
  * Paginated writes none of those: the turns are `response_item` lines of type
144
163
  * `message` with a `role` and a `content` array of `input_text`, `input_image`
145
- * and `output_text` parts. Measured 2026-09-15 across the seven days before:
146
- * all 61 of her own threads were paginated, and the first build read only
147
- * legacy, which is why her row said "Codex has not answered yet." */
164
+ * and `output_text` parts. Current Codex threads are paginated, and a build
165
+ * that read only legacy showed "Codex has not answered yet." on every row. */
148
166
  export function readRollout(file) {
149
167
  let text = '';
150
168
  try {
@@ -177,7 +195,12 @@ export function readRollout(file) {
177
195
  meta = {
178
196
  id: String(p.session_id ?? p.id ?? '').trim(),
179
197
  cwd: String(p.cwd ?? '').trim(),
180
- threadSource: typeof p.thread_source === 'string' ? p.thread_source : (typeof p.source === 'string' ? 'user' : 'other'),
198
+ // A RUN THIS APP STARTED IS NOT HERS TO IMPORT (w-db6f5e331e). Codex
199
+ // 0.160.0 writes no thread_source, so the line after this would call
200
+ // every run a person's; its originator is the clientInfo name we hand
201
+ // Codex, under this name or one the app had before.
202
+ threadSource: OUR_ORIGINATORS.has(String(p.originator ?? '').toLowerCase()) ? 'agentbox'
203
+ : typeof p.thread_source === 'string' ? p.thread_source : (typeof p.source === 'string' ? 'user' : 'other'),
181
204
  startedAt: Number.isFinite(startedAt) ? startedAt : 0,
182
205
  };
183
206
  continue;
@@ -227,15 +250,15 @@ export const LIVE_MS = 2 * 60_000;
227
250
  export const isLive = (mtime, now = Date.now()) => Number(mtime) > 0 && now - Number(mtime) < LIVE_MS;
228
251
 
229
252
  /**
230
- * HER CONVERSATIONS, newest first, in the shape the import card and the
231
- * import row both read. One entry per thread she typed into, in a real folder,
253
+ * THE USER'S CONVERSATIONS, newest first, in the shape the import card and the
254
+ * import row both read. One entry per thread a person typed into, in a real folder,
232
255
  * touched in the last `days` days. */
233
256
  export function readCodexThreads({ home = os.homedir(), codexDir = codexHome(home), days = CODEX_RECENT_DAYS, now = Date.now() } = {}) {
234
257
  const index = readCodexIndex(codexDir);
235
258
  const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
236
259
  const out = [];
237
260
  const skipped = { notHers: 0, scratch: 0, old: 0, empty: 0, unreadable: 0 };
238
- for (const file of listRolloutFiles(codexDir, { days })) {
261
+ for (const file of listRolloutFiles(codexDir, { days, since: cutoff })) {
239
262
  const r = readRollout(file);
240
263
  if (!r) { skipped.unreadable += 1; continue; }
241
264
  if (r.threadSource !== 'user') { skipped.notHers += 1; continue; }
@@ -4,17 +4,17 @@
4
4
  // and it is a good one: never spawn anything and never ask OpenAI anything to
5
5
  // draw a picture. Codex's figures ride the app-server the fleet is already
6
6
  // running and arrive unprompted during a turn. The unstated cost of that rule is
7
- // that the app only ever learns a number for work POWERUP ITSELF RAN. She runs
8
- // Codex in its own app all day, so on her Mac the push had never arrived and the
9
- // panel was empty while her weekly limit sat at 100% used.
7
+ // that the app only ever learns a number for work POWERUP ITSELF RAN. Someone
8
+ // who runs Codex in its own app all day never gets the push, so the panel sits
9
+ // empty while their weekly limit is at 100% used.
10
10
  //
11
11
  // SO THERE IS A THIRD SOURCE, AND IT BREAKS NEITHER HALF OF THAT RULE. Codex
12
12
  // writes every rate-limit report it receives into its own session log under
13
13
  // CODEX_HOME, as a `rate_limits` object on the `token_count` events. Reading it
14
14
  // starts no process, sends nothing anywhere, and touches no credential: it is a
15
- // file on her disk that Codex put there. Measured on her Mac 2026-09-18: 1,140
16
- // such records across her twenty-five most recent sessions, the newest written
17
- // the same minute she filed the row.
15
+ // file on the disk that Codex put there. A busy Codex home holds hundreds of
16
+ // such records across its most recent sessions, the newest written within
17
+ // the minute.
18
18
  //
19
19
  // THE PUSH STILL WINS. A live notification is this second's truth and a log is
20
20
  // the last thing anybody wrote down, so main/codex-usage.mjs reads this only when
@@ -111,7 +111,7 @@ function tail(file, bytes = TAIL_BYTES) {
111
111
  *
112
112
  * Lines are walked backwards because the last report is the one that is true. A
113
113
  * report carrying no usable window is skipped rather than accepted: Codex emits
114
- * an all-null snapshot for limit families that do not apply to the account (her
114
+ * an all-null snapshot for limit families that do not apply to the account (a
115
115
  * `premium` family, every field null), and taking one of those would blank a
116
116
  * panel that had a real figure a moment earlier.
117
117
  */
@@ -11,7 +11,7 @@
11
11
  // Codex's figures ride the `codex app-server` THE FLEET IS ALREADY RUNNING:
12
12
  // they arrive unprompted as `account/rateLimits/updated` during a turn.
13
13
  //
14
- // AND NEVER ASK FOR ONE EITHER (the founder, 2026-09-05). This reader briefly
14
+ // AND NEVER ASK FOR ONE EITHER. This reader briefly
15
15
  // also sent an explicit rate-limits read down that pipe whenever its figure
16
16
  // went stale. It is deliberately gone, and this paragraph is here so the next
17
17
  // reader does not put it back as an obvious improvement.
@@ -96,9 +96,9 @@ export class CodexUsage {
96
96
  const limits = codexLimits(this._snapshot);
97
97
  if (limits.length) return { limits, at: this._at };
98
98
  // NOTHING HAS PUSHED, SO ASK WHAT CODEX ALREADY WROTE DOWN. This is the
99
- // whole of: the push only ever arrives for work the app itself ran, and she
100
- // runs Codex in its own app, so on her Mac the panel stayed empty while her
101
- // weekly limit was full. main/codex-usage-file.mjs is a read of a file
99
+ // whole of: the push only ever arrives for work the app itself ran, and a
100
+ // person who runs Codex in its own app would see the panel stay empty
101
+ // while their weekly limit was full. main/codex-usage-file.mjs is a read of a file
102
102
  // under this same CODEX_HOME. It starts no process and sends nothing
103
103
  // anywhere, so neither rule above is bent.
104
104
  return this._logged();
@@ -1,25 +1,25 @@
1
- // THE WATCH THAT NOTICES A CODEX CONVERSATION AND ASKS.
1
+ // THE WATCH THAT KEEPS AN IMPORTED CODEX CONVERSATION CURRENT.
2
2
  //
3
- // Her pick on 19:53: "ask first". So every minute this reads the conversations
4
- // she had in Codex this week, and for each one that runs in a folder one of her
5
- // projects points at and has no row anywhere yet, it files the asking row in
6
- // that project. It also keeps the rows she said yes to current: when Codex
7
- // writes another answer, the row's result follows it.
3
+ // Every minute this reads the user's recent Codex conversations and, for each
4
+ // one they already brought in, makes the row follow Codex: when Codex writes
5
+ // another answer, the row's result follows it. An asking row filed before this
6
+ // change is kept true the same way.
8
7
  //
9
- // A conversation in a folder no project points at gets no row here. It is
10
- // still on the ⌘K import card, under a section that makes the project on the
11
- // press, which is the card's own way of handling a new folder.
8
+ // IT NO LONGER ASKS ABOUT NEW ONES (w-db6f5e331e, 2026-10-05). It used to file
9
+ // an "Import into ...?" row for every new conversation in a project's folder.
10
+ // The founder, before launch: "after onboarding for both anthropic and openai
11
+ // harnesses, it should only import on command, not continuously". Importing is
12
+ // now the walk's last card and ⌘K import, for Claude Code and Codex alike.
12
13
  import { readCodexThreads } from './codex-threads.mjs';
13
14
 
14
15
  export const CODEX_POLL_MS = 60_000;
15
16
 
16
17
  export function scanCodex({ store, readThreads = readCodexThreads, now = Date.now() } = {}) {
17
18
  let threads = [];
18
- try { ({ threads } = readThreads({ now })); } catch { return { asked: 0, refreshed: 0, retold: 0, threads: 0 }; }
19
- const asked = store.askAboutCodexThreads(threads, { now });
19
+ try { ({ threads } = readThreads({ now })); } catch { return { refreshed: 0, retold: 0, threads: 0 }; }
20
20
  const refreshed = store.refreshCodexMirrors(threads, { now });
21
21
  const retold = store.refreshCodexAsks(threads, { now });
22
- return { asked, refreshed, retold, threads: threads.length };
22
+ return { refreshed, retold, threads: threads.length };
23
23
  }
24
24
 
25
25
  export function startCodexWatch({ store, readThreads = readCodexThreads, pollMs = CODEX_POLL_MS, firstDelayMs = 8_000 } = {}) {
package/main/config.mjs CHANGED
@@ -59,10 +59,30 @@ const DEFAULTS = {
59
59
  // How many sessions run at once. The queue absorbs the rest; when the plan
60
60
  // window caps out, a longer queue is the visible, benign failure mode.
61
61
  //
62
- // THIS IS THE NUMBER FOR A MAX SUBSCRIPTION, which is the only kind this was
63
- // ever chosen on. When nobody has set one, loadConfig below reads the plan
62
+ // THIS IS THE NUMBER FOR A MAX SUBSCRIPTION, the biggest plan. When nobody
63
+ // has set one, loadConfig below reads the plan
64
64
  // Claude Code is signed in on and lowers this for a smaller one.
65
65
  maxConcurrentSessions: DEFAULT_SESSIONS_AT_ONCE,
66
+ // LET AGENTBOX DECIDE HOW MANY RUN (w-e5225b62ba). On unless somebody has set
67
+ // a number themselves, which `loadConfig` works out below from whether one was
68
+ // ever written down. With it on, `maxConcurrentSessions` is not read at all:
69
+ // `Supervisor#_slotsPerAccount` asks main/machine.mjs for what this Mac
70
+ // carries and shares that out across the live accounts. `agentsNudge` is how
71
+ // many times "Something felt slow" has been pressed, and only ever subtracts.
72
+ agentsAuto: true,
73
+ agentsNudge: 0,
74
+ // HOLD HEAVY WORK WHEN MEMORY IS SHORT (w-3958c3753d). Off until somebody
75
+ // turns it on from the Agents page. With it on, every shell command a worker
76
+ // runs asks main/memory-gate-server.mjs first, and heavy ones wait their turn
77
+ // while the Mac is short of memory. `memoryGateSlots` is how many heavy
78
+ // commands may run at once; null is Auto, one per 8 GB.
79
+ memoryGate: false,
80
+ memoryGateSlots: null,
81
+ // STOP WHAT FINISHED AGENTS LEAVE RUNNING (main/leftovers.mjs). Its own
82
+ // switch, off until somebody turns it on; `cleanupLeftoversSince` is the
83
+ // moment they did, so nothing is stopped sooner than the idle time after it.
84
+ cleanupLeftovers: false,
85
+ cleanupLeftoversSince: null,
66
86
  // What spawned sessions are allowed to do. Left unset on purpose, because the
67
87
  // right default now depends on whether a store MCP server is present: see
68
88
  // Supervisor#sessionArgsFor. Setting it here still wins over that, which is
@@ -80,11 +100,9 @@ const DEFAULTS = {
80
100
  // PERSONAL PROJECTS AND THE DRIVE WERE BOTH DECLARED HERE AND BOTH ARE GONE
81
101
  // (w-d19d6d387c, 2026-09-22). A personal project ran its sessions on the
82
102
  // founder's message and nothing else, with no brief and no store; the drive
83
- // asked "what next?" by itself on a project that had gone quiet. Hers, on
84
- // the same day: "I would get rid of 'personal' as well. I think there is no
85
- // case where we should ever have that", and "we should get rid of the code
86
- // that self-drives the product entirely. That should never have existed
87
- // anyway, especially because we're open sourcing it soon."
103
+ // asked "what next?" by itself on a project that had gone quiet. There is no
104
+ // case where a project should run without a brief and a store, and code that
105
+ // drives a product on its own does not belong in an open-source app.
88
106
  //
89
107
  // A config still carrying `personalProducts` or `driveEnabled` is not an
90
108
  // error; nothing reads either key now.
@@ -150,11 +168,20 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
150
168
  // The account id, minted once per install. Written back immediately, because
151
169
  // a generated id that is not saved is a different id next launch and the
152
170
  // store would move out from under everything.
171
+ //
172
+ // AND A NEW INSTALL MAY CHOOSE CODEX FROM ITS FIRST LAUNCH (w-db6f5e331e).
173
+ // `engineChoice` is the moment from which a row may run on Codex; it exists
174
+ // so August rows in one old store never move engine unasked, and it could
175
+ // only be written by hand, so a new person with both engines never got
176
+ // Codex at all. A Mac with no config file has no old rows, so its first
177
+ // launch is that moment. An install with a config is left as it was.
178
+ const firstLaunch = fresh && !config.engineChoice ? new Date().toISOString() : null;
179
+ if (firstLaunch) config.engineChoice = firstLaunch;
153
180
  if (typeof config.accountId !== 'string' || !config.accountId) {
154
181
  config.accountId = crypto.randomUUID();
155
182
  try {
156
183
  fs.mkdirSync(path.dirname(file), { recursive: true });
157
- fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId }, null, 2)}\n`);
184
+ fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId, ...(firstLaunch ? { engineChoice: firstLaunch } : {}) }, null, 2)}\n`);
158
185
  } catch (err) {
159
186
  console.warn(`zero: could not write the new account id to ${file}: ${err.message}`);
160
187
  }
@@ -205,6 +232,11 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
205
232
  config.codexFound = codex.found;
206
233
  config.codexCertain = codex.certain !== false;
207
234
  config.codexFrom = codex.from;
235
+ // AND AN OLDER INSTALL WITH CODEX ON IT OPENS THE CHOICE TOO (w-d5d632e503).
236
+ // Only a Mac with no config file did, so a person who installed before
237
+ // that, or who added Codex later, never saw Codex anywhere. Written at the
238
+ // end, once `appDir` is on the config to save through.
239
+ const codexOpened = codexChoiceToOpen(config, codex.found);
208
240
  // HOW MANY AGENTS AT ONCE, WHEN NOBODY HAS SAID, IS THE PLAN AND THE MACHINE
209
241
  // TOGETHER. Somebody on a smaller plan gets several sessions started for them,
210
242
  // they all spend the same allowance, and what they see is agents that are slow
@@ -237,6 +269,16 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
237
269
  // already in zero.config.json is untouched, for the same reason the plan cap
238
270
  // leaves it alone.
239
271
  config.machineSlots = machineSlots().slots;
272
+ // WHO IS DECIDING THE NUMBER, read off whether one was ever written down
273
+ // (w-e5225b62ba). A number in zero.config.json is somebody's own instruction
274
+ // about this Mac and keeps being obeyed; an install that never touched the
275
+ // stepper was already having its number filled in here, and Automatic is that
276
+ // same thing said out loud and kept up to date. An explicit `agentsAuto`
277
+ // outranks the guess, because turning Automatic back on after setting a number
278
+ // has to be possible.
279
+ if (typeof overrides.agentsAuto !== 'boolean') {
280
+ config.agentsAuto = typeof overrides.maxConcurrentSessions !== 'number';
281
+ }
240
282
  if (typeof overrides.maxConcurrentSessions !== 'number') {
241
283
  const plans = readPlans(effectiveProfiles(config.authProfiles, { home }), { home });
242
284
  const slots = slotsForPlans(plans, config.machineSlots);
@@ -254,9 +296,27 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
254
296
  // Where the file is, so anything that edits a setting can write it back
255
297
  // without being handed appDir a second time.
256
298
  config.appDir = appDir;
299
+ if (codexOpened) {
300
+ try {
301
+ saveConfig(config, { engineChoice: codexOpened });
302
+ } catch (err) {
303
+ console.warn(`zero: could not open the Codex choice in ${file}: ${err.message}`);
304
+ }
305
+ }
257
306
  return config;
258
307
  }
259
308
 
309
+ /**
310
+ * THE MOMENT TO OPEN THE CODEX CHOICE AT, OR NULL. Now, when Codex is found
311
+ * and the key was never written. NOW is what keeps a row marked `codex` in
312
+ * August on Claude Code: it was written before this moment, so it is stale
313
+ * (`engineChoiceOnRowIsStale`). A moment already written is kept, and
314
+ * `"engineChoice": false` written by hand keeps the choice shut. */
315
+ export function codexChoiceToOpen(config, codexFound) {
316
+ if (!codexFound || config.engineChoice != null) return null;
317
+ return new Date().toISOString();
318
+ }
319
+
260
320
  export function configFile(appDir) {
261
321
  return path.join(appDir, CONFIG_NAME);
262
322
  }
@@ -1,16 +1,16 @@
1
- // A HOT-RELOADING WINDOW MUST NOT BE THE ONE SHE WORKS IN.
1
+ // A HOT-RELOADING WINDOW MUST NOT BE THE ONE THE USER WORKS IN.
2
2
  //
3
- // She was working in a window an agent had started against a vite dev server,
4
- // on her real store. Vite rebuilds that window whenever the source under it
5
- // changes, and the source under it is the checkout every agent shares: it was
6
- // checked out or merged five times in the eleven minutes before she wrote.
3
+ // A window an agent starts against a vite dev server can end up running on the
4
+ // real store. Vite rebuilds that window whenever the source under it changes,
5
+ // and the source under it is the checkout every agent shares, which can be
6
+ // checked out or merged several times in a few minutes.
7
7
  //
8
- // The one that gets her is a change to App.tsx that adds, removes or moves a
8
+ // The change that hurts is one to App.tsx that adds, removes or moves a
9
9
  // hook, which is what a branch switch does to it. React Fast Refresh keeps a
10
10
  // component's state only while its hooks match, so App is rebuilt from nothing:
11
11
  // `snap` is null again, App.tsx draws `<div className="boot">the app</div>`, and
12
- // every child of App is thrown away and remade. That is the screen she means by
13
- // the zero loading page, and that is the artifact closing and reopening. The
12
+ // every child of App is thrown away and remade. The window drops back to the
13
+ // loading page, and an open artifact closes and reopens. The
14
14
  // page never navigates, which is why nothing in the dev server's log looked bad
15
15
  // enough to be it. Six triggers were measured against a real window and exactly
16
16
  // one was guilty: scripts/what-throws-her-out-of-agentbox.mjs.
@@ -25,7 +25,7 @@
25
25
  // WHAT IS STILL ALLOWED, because a guard that stops the work is a guard somebody
26
26
  // deletes:
27
27
  //
28
- // Fixtures. Nothing in a fixtures window is hers, so there is nothing to be
28
+ // Fixtures. Nothing in a fixtures window is real, so there is nothing to be
29
29
  // thrown out of, and this is what screenshot and proof scripts use.
30
30
  //
31
31
  // A throwaway store. A script that points HOME at a temp directory has no
@@ -124,3 +124,5 @@ export class DocGrants {
124
124
  // main/zoom-keys.mjs) scales the window, and the pane is an ordinary iframe in
125
125
  // that window, so an open file follows it like everything else. Do not put a
126
126
  // listener back into a served page: nothing is injected into a file she reads.
127
+ // The one thing added to a page is its scrollbar's look, from outside and after
128
+ // it is served, and frame-scrollbars.mjs says why that is not this.
@@ -0,0 +1,193 @@
1
+ // SETTING UP SOMEBODY'S PLAN FOR THEM (w-9f6975906c, picked 2026-10-05).
2
+ //
3
+ // A person with a Claude or ChatGPT plan and no Claude Code or Codex used to
4
+ // reach a card with a link to an install page. Now the walk asks which plan
5
+ // they pay for and this does the rest: install the tool if it is missing, start
6
+ // its own sign-in, and wait until the tool itself says it is signed in. The one
7
+ // thing left for them is approving in their browser.
8
+ //
9
+ // EVERY COMMAND HERE IS THE TOOL'S OWN, and that is a rule, not a style.
10
+ // Anthropic does not allow third party apps to offer claude.ai login unless
11
+ // approved, so Agentbox never draws a sign-in of its own: it runs Claude Code's
12
+ // `auth login`, which opens Anthropic's page, and Codex's `login`, which opens
13
+ // OpenAI's. Measured 2026-10-05 into throwaway home folders with stdin closed:
14
+ // both installers finish with no prompts (Claude Code 24.6 s, Codex 9.7 s) and
15
+ // land in `~/.local/bin`, the first place each finder looks; `claude auth login
16
+ // --claudeai` runs `open <url>` with a localhost callback, so approving in the
17
+ // browser finishes it with nothing to paste.
18
+ //
19
+ // NO API KEY REACHES ANYTHING RUN HERE. An inherited ANTHROPIC_API_KEY makes
20
+ // Claude Code report itself signed in on the key and bill it, which is the
21
+ // opposite of setting up their plan; `terminalEnv` is the scrub the built-in
22
+ // terminal already uses.
23
+
24
+ import { spawn as nodeSpawn } from 'node:child_process';
25
+ import { terminalEnv } from './task-terminals.mjs';
26
+
27
+ const INSTALLERS = {
28
+ claude: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
29
+ codex: { shell: '/bin/sh', url: 'https://chatgpt.com/codex/install.sh', pipe: 'sh' },
30
+ };
31
+
32
+ export function installCommand(engine) {
33
+ const i = INSTALLERS[engine];
34
+ return { file: i.shell, args: ['-c', `curl -fsSL ${i.url} | ${i.pipe}`] };
35
+ }
36
+
37
+ /** The plan, never the API console: `--claudeai` is Claude Code's own name for it. */
38
+ export function signInCommand(engine, bin) {
39
+ return engine === 'claude'
40
+ ? { file: bin, args: ['auth', 'login', '--claudeai'] }
41
+ : { file: bin, args: ['login'] };
42
+ }
43
+
44
+ /** Exit 0 is signed in and 1 is not, for both (measured 2026-10-05). */
45
+ export function statusCommand(engine, bin) {
46
+ return engine === 'claude'
47
+ ? { file: bin, args: ['auth', 'status', '--json'] }
48
+ : { file: bin, args: ['login', 'status'] };
49
+ }
50
+
51
+ const LOG_LIMIT = 64 * 1024;
52
+ const STATUS_TIMEOUT = 15_000;
53
+ const POLL = 2_000;
54
+ const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
55
+
56
+ function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
57
+ let child;
58
+ try {
59
+ child = nodeSpawn(file, args, { env, stdio: [keepStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
60
+ } catch {
61
+ return { exit: Promise.resolve(127), kill() {} };
62
+ }
63
+ const feed = (d) => onData?.(String(d));
64
+ child.stdout?.on('data', feed);
65
+ child.stderr?.on('data', feed);
66
+ const exit = new Promise((resolve) => {
67
+ child.on('exit', (code) => resolve(code ?? 1));
68
+ child.on('error', () => resolve(127));
69
+ });
70
+ return { exit, kill: () => { try { child.kill('SIGTERM'); } catch { /* already gone */ } } };
71
+ }
72
+
73
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
74
+
75
+ /**
76
+ * One setup per tool, held for the life of the app.
77
+ *
78
+ * `find(engine)` is the finder, looking again from scratch (the caller passes
79
+ * `recheckClaude` / `recheckCodex`, which also move the config so the rest of
80
+ * the app runs on what was just installed). `spawn` and `wait` are seams for
81
+ * the tests; nothing in the app passes them.
82
+ */
83
+ export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
84
+ const runEnv = terminalEnv(env);
85
+ const jobs = new Map();
86
+
87
+ const fresh = (engine) => ({ engine, phase: 'idle', error: null, log: '', running: null, login: null, run: 0 });
88
+ const job = (engine) => { if (!jobs.has(engine)) jobs.set(engine, fresh(engine)); return jobs.get(engine); };
89
+ const say = (j, text) => { j.log = (j.log + String(text).replace(ANSI, '')).slice(-LOG_LIMIT); };
90
+ const lastLine = (text) => text.split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
91
+
92
+ async function signedIn(j, bin) {
93
+ const { file, args } = statusCommand(j.engine, bin);
94
+ const p = spawn(file, args, { env: runEnv });
95
+ const TIMED_OUT = Symbol('no answer');
96
+ const code = await Promise.race([p.exit, wait(STATUS_TIMEOUT).then(() => { p.kill(); return TIMED_OUT; })]);
97
+ // TOLD TO THE APP, which routes on it (w-d5d632e503): a Claude Code that
98
+ // is signed out beside a Codex that is signed in is a Mac that runs on
99
+ // Codex. Only a real yes or no; a check that never answered says nothing.
100
+ if (code === 0 || code === 1) onSignIn(j.engine, code === 0);
101
+ return code === 0;
102
+ }
103
+
104
+ function startLogin(j, bin) {
105
+ j.login?.kill();
106
+ const { file, args } = signInCommand(j.engine, bin);
107
+ const login = spawn(file, args, { env: runEnv, keepStdin: true, onData: (d) => say(j, d) });
108
+ j.login = login;
109
+ return login;
110
+ }
111
+
112
+ async function go(j, run) {
113
+ const live = () => j.run === run;
114
+ let hit = find(j.engine);
115
+ if (!hit?.found) {
116
+ j.phase = 'installing';
117
+ const { file, args } = installCommand(j.engine);
118
+ const p = spawn(file, args, { env: runEnv, onData: (d) => say(j, d) });
119
+ j.running = p;
120
+ const code = await p.exit;
121
+ j.running = null;
122
+ if (!live()) return;
123
+ if (code !== 0) return fail(j, `The installer stopped: ${lastLine(j.log) || `exit ${code}`}`);
124
+ hit = find(j.engine);
125
+ if (!hit?.found) return fail(j, 'The installer finished, but the app still cannot find it.');
126
+ }
127
+ j.bin = hit.path;
128
+ j.phase = 'checking';
129
+ if (await signedIn(j, hit.path)) return live() && ready(j);
130
+ if (!live()) return;
131
+ j.phase = 'signing-in';
132
+ let login = startLogin(j, hit.path);
133
+ let gaveUp = false;
134
+ login.exit.then((code) => { if (login === j.login && code !== 0) gaveUp = true; });
135
+ for (;;) {
136
+ await wait(poll);
137
+ if (!live()) return;
138
+ if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
139
+ if (j.login !== login) {
140
+ login = j.login;
141
+ gaveUp = false;
142
+ login.exit.then((code) => { if (login === j.login && code !== 0) gaveUp = true; });
143
+ }
144
+ if (gaveUp) return fail(j, 'The sign-in was closed before it finished.');
145
+ }
146
+ }
147
+
148
+ function ready(j) { j.login?.kill(); j.login = null; j.phase = 'ready'; j.error = null; }
149
+ function fail(j, error) { j.login?.kill(); j.login = null; j.phase = 'failed'; j.error = error; }
150
+
151
+ return {
152
+ start(engine) {
153
+ const j = job(engine);
154
+ if (['installing', 'checking', 'signing-in'].includes(j.phase)) return this.status(engine);
155
+ j.run += 1; j.phase = 'checking'; j.error = null; j.log = '';
156
+ const run = j.run;
157
+ go(j, run).catch((e) => { if (j.run === run) fail(j, String(e?.message ?? e)); });
158
+ return this.status(engine);
159
+ },
160
+ /** Kill the sign-in that is waiting and start a new one, which opens the page again. */
161
+ signInAgain(engine) {
162
+ const j = job(engine);
163
+ if (j.phase === 'signing-in' && j.bin) startLogin(j, j.bin);
164
+ return this.status(engine);
165
+ },
166
+ cancel(engine) {
167
+ const j = job(engine);
168
+ j.run += 1;
169
+ j.running?.kill(); j.login?.kill();
170
+ j.running = null; j.login = null;
171
+ if (j.phase !== 'ready') j.phase = 'idle';
172
+ return this.status(engine);
173
+ },
174
+ status(engine) {
175
+ const j = job(engine);
176
+ return { engine, phase: j.phase, error: j.error, log: j.log };
177
+ },
178
+ /**
179
+ * WHETHER THIS TOOL CAN RUN AN AGENT RIGHT NOW: found, and signed in by its
180
+ * own account. The walk asks before it asks anything, so a Mac that is
181
+ * already set up never sees the plan question. */
182
+ async readiness(engine) {
183
+ const hit = find(engine);
184
+ if (!hit?.found) return { engine, found: false, signedIn: false };
185
+ return { engine, found: true, signedIn: await signedIn({ engine }, hit.path) };
186
+ },
187
+ /** The same question against a path already found, so asking it at
188
+ * launch does not search the Mac again. */
189
+ signedInNow(engine, bin) {
190
+ return signedIn({ engine }, bin);
191
+ },
192
+ };
193
+ }