agentbox-app 0.1.7 → 0.1.12

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 (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -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 +11 -11
  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 +116 -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/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
@@ -0,0 +1,87 @@
1
+ // A MESSAGE FROM A TEAMMATE IS SORTED BY HOW URGENT IT IS (2026-10-01): the
2
+ // agent reads each message and gives it a priority, so the reader does not
3
+ // have to.
4
+ //
5
+ // A message is made with priority 0 and nothing ever moved it, so the inbox's
6
+ // priority sort could not tell "the checkout page is down" from "thanks!". Now
7
+ // the Mac the message waits on asks the same small model the namer uses
8
+ // (main/row-label.mjs) for one of the four levels, once per message, and
9
+ // writes it onto the row as an agent. A level the person set by hand wins, by
10
+ // the fold's own rule, and this does not ask again after one.
11
+ import { askSmallModel } from './row-label.mjs';
12
+
13
+ /** The four words, onto the app's own values (renderer/src/priority.ts). */
14
+ export const LEVELS = { urgent: 9, high: 7, medium: 5, low: 2 };
15
+
16
+ const LIVE = new Set(['open', 'claimed', 'blocked']);
17
+
18
+ /**
19
+ * PURE. The newest message on a conversation: its words, who wrote it and
20
+ * when. A reply is the newest when there is one; otherwise the opening body.
21
+ */
22
+ export function latestMessage(item) {
23
+ const a = item?.wrote?.answer;
24
+ const b = item?.wrote?.body;
25
+ const answered = !!a && !!item?.answer && item.answer !== '(withdrawn)' && (!b || a.ts >= b.ts);
26
+ const w = answered ? a : b;
27
+ const text = String((answered ? item.answer : item?.body) ?? '').trim();
28
+ if (!w || !text) return null;
29
+ return { text, by: w.by ?? null, ts: w.ts ?? 0 };
30
+ }
31
+
32
+ /**
33
+ * PURE. Does this row hold a message for me to sort? Only a conversation that
34
+ * is still going, waiting on me, whose newest message someone else wrote, and
35
+ * which has not been sorted since that message. Never one whose priority the
36
+ * person set by hand: the fold keeps theirs over ours, so asking would only
37
+ * repeat forever.
38
+ */
39
+ export function wantsPriority(item, product, me) {
40
+ if (!me || !item || item.agent) return false;
41
+ if (!product?.team?.direct) return false;
42
+ if (!LIVE.has(item.status)) return false;
43
+ if (item.assignee !== me) return false;
44
+ const latest = latestMessage(item);
45
+ if (!latest || !latest.by || latest.by === me) return false;
46
+ const set = item.wrote?.priority;
47
+ if (set?.source === 'founder') return false;
48
+ // AND NEVER OVER THE LEVEL THE SENDER PICKED (w-7ba439c883). A message now
49
+ // crosses carrying how urgent the person who wrote it thought it was, and a
50
+ // guess from here would overwrite it: the sender's line is composeItem's, so
51
+ // it claims no authority the fold would defend, and a level written from here
52
+ // counts for nothing anyway (an agent's tag, shared/rank.mjs itemPriority).
53
+ // A row nobody chose a level for is composeItem's 0, so a number above it is
54
+ // exactly "somebody picked one", and `by` says it was not this Mac.
55
+ if (set?.by && set.by !== me && Number.isFinite(item.priority) && item.priority > 0) return false;
56
+ return !(set && set.source !== 'system' && set.ts >= latest.ts);
57
+ }
58
+
59
+ /** PURE. The one question, about the message alone, cut short. */
60
+ export function priorityPrompt(latest) {
61
+ const text = String(latest?.text ?? '').replace(/\r/g, '').trim().slice(0, 1500);
62
+ return [
63
+ 'A teammate sent this message to the person you work for. How soon do they',
64
+ 'need to read it and answer? Reply with one word: urgent, high, medium or low.',
65
+ '',
66
+ 'urgent: something is broken or blocked now, or it is due within hours.',
67
+ 'high: it needs an answer today.',
68
+ 'medium: an ordinary question or request.',
69
+ 'low: a thank you, an FYI or a social note that needs no answer.',
70
+ '',
71
+ '--- message ---',
72
+ text,
73
+ '---',
74
+ ].join('\n');
75
+ }
76
+
77
+ /** PURE. The model's reply as one of the four words, or '' when it is not one. */
78
+ export function cleanPriority(text) {
79
+ const first = String(text ?? '').split('\n').map((l) => l.trim()).filter(Boolean)[0] ?? '';
80
+ const word = first.replace(/^["'`“”‘’]+|["'`“”‘’.!]+$/g, '').trim().toLowerCase();
81
+ return Object.hasOwn(LEVELS, word) ? word : '';
82
+ }
83
+
84
+ /** Ask the small model about one message. '' on every failure, never a throw. */
85
+ export function sortMessage(latest, opts = {}) {
86
+ return askSmallModel(priorityPrompt(latest), cleanPriority, opts);
87
+ }
package/main/notify.mjs CHANGED
@@ -104,7 +104,7 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
104
104
  // two-minute diagnosis.
105
105
  n.on('failed', (_e, err) => {
106
106
  live = null;
107
- console.warn(`zero: macOS refused the notification (${err}). Check ${NAME} in System Settings > Notifications.`);
107
+ console.warn(`zero: the system refused the notification (${err}). Check ${NAME} in the system notification settings.`);
108
108
  });
109
109
  live = n;
110
110
  try {
@@ -27,8 +27,8 @@ import path from 'node:path';
27
27
  export const NOTE_NAME = 'pinned.md';
28
28
 
29
29
  // Far past anything a sidebar column can hold, and still small enough that a
30
- // runaway paste cannot fill her disk. A note that hits this is refused rather
31
- // than truncated: half her words written back is worse than none.
30
+ // runaway paste cannot fill the disk. A note that hits this is refused rather
31
+ // than truncated: half a note written back is worse than none.
32
32
  export const MAX_NOTE_BYTES = 200_000;
33
33
 
34
34
  /** The file this product's sidebar note lives in, or null if it has no folder. */
@@ -0,0 +1,21 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {createCodexAppServer} from './codex-app-server.mjs';
3
+ import {rememberCodexModels} from './codex-models.mjs';
4
+ import {forget,claudeModels} from './claude-models.mjs';
5
+ import {terminalEnv} from './task-terminals.mjs';
6
+ import {codexLaunchEnv} from './codex-launch-env.mjs';
7
+ export async function refreshAgentModels(engine,config,home){
8
+ if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
9
+ // Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
10
+ if(!config.codexBin)return;
11
+ const client=createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
12
+ const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
13
+ try{
14
+ await client.initialize();let cursor=null;const models=[];const seen=new Set();
15
+ do{const r=await client.request('model/list',{limit:100,includeHidden:true,...(cursor?{cursor}:{})});
16
+ if(!Array.isArray(r.data))throw Error('Model list could not be read.');models.push(...r.data);cursor=r.nextCursor;
17
+ if(cursor&&seen.has(cursor))throw Error('Model list did not finish.');seen.add(cursor);
18
+ }while(cursor);
19
+ if(!models.length)throw Error('No models were returned.');rememberCodexModels(home,models);
20
+ }finally{clearTimeout(deadline);client.close('Model refresh complete');}
21
+ }
package/main/repeats.mjs CHANGED
@@ -36,7 +36,7 @@ function describeRule({ every, at, on }) {
36
36
  // How many periods in a row may be superseded before the founder is told.
37
37
  const MISSES_BEFORE_ALERT = 3;
38
38
 
39
- // The protocol goes FIRST in an occurrence's body, ahead of her words. An
39
+ // The protocol goes FIRST in an occurrence's body, ahead of the user's instruction. An
40
40
  // over-long body keeps its prefix and cuts its suffix, so a protocol written
41
41
  // last can be truncated away entirely, and a worker that never saw it cannot
42
42
  // know that a quiet finish has to be marked.
@@ -112,11 +112,11 @@ export class Repeats {
112
112
  title, body, priority = 5, every = 'day', at, on, engine, model,
113
113
  }, now = Date.now()) {
114
114
  // Refused here rather than stored and ignored later: a rule that cannot be
115
- // kept has to fail while she is still looking at what she typed.
115
+ // kept has to fail while the user is still looking at what they typed.
116
116
  //
117
117
  // `on` is validated AND stored. It was in neither, so a weekly rule was
118
118
  // checked without the day that makes it weekly, and no weekly rule ever
119
- // survived this line (hers). The refusal says which day is missing now,
119
+ // survived this line. The refusal says which day is missing now,
120
120
  // because "every week at 08:00" reads like a bug in the clock.
121
121
  if (!title || !String(title).trim()) throw new Error('a repeating task needs a title');
122
122
  if (!isRepeatRule({ every, at, on })) throw new Error(`not a schedule ${NAME} can keep: ${describeRule({ every, at, on })}`);
@@ -134,7 +134,7 @@ export class Repeats {
134
134
  patchRule(productDir, id, patch, now = Date.now()) {
135
135
  // `on` joins the other two: changing a daily rule to a Thursday one sends
136
136
  // every, at AND on, and checking the first two alone refused the change for
137
- // the same reason a new weekly rule was refused (hers).
137
+ // the same reason a new weekly rule was refused.
138
138
  if (('at' in patch || 'every' in patch || 'on' in patch)) {
139
139
  const current = this.get(productDir, id, now);
140
140
  if (!current) throw new Error(`no such repeating task: ${id}`);
@@ -1,47 +1,46 @@
1
- // THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM HER FIRST SENTENCE.
1
+ // THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM THE FIRST SENTENCE.
2
2
  //
3
- // A message she dictates keeps its first line as its title, verbatim and
4
- // forever (renderer/src/message-split.ts). Measured on her Agentbox store the day
5
- // she asked: 214 of the 330 items are hers, median title 84 characters, 145 over
6
- // 60, 37 cut off mid sentence with an ellipsis, and 76 opening with "I", "I've
7
- // been", "Can you" or "We". Eleven of those down a column is eleven rows that
8
- // look like one row.
3
+ // A dictated message keeps its first line as its title, verbatim and forever
4
+ // (renderer/src/message-split.ts). Such titles run long, are often cut off mid
5
+ // sentence with an ellipsis, and many open with "I", "I've been", "Can you" or
6
+ // "We". Eleven of those down a column is eleven rows that look like one row.
9
7
  //
10
8
  // THERE IS NOTHING TO EXTRACT FROM CLAUDE CODE, which is the first thing this
11
9
  // had to find out. Claude Code does generate a short name for a session and it
12
- // writes it to the terminal tab; it never puts it on disk. Measured 2026-09-03:
13
- // ZERO of her 3,126 transcript files written since 20 August carry a `summary`
14
- // row, and main/agent-sessions.mjs found the same over 495 files on 08-29. So
15
- // the name is not read from anywhere. It is written, here, by the same kind of
16
- // call Claude Code makes: one small fast model, one message, one line back.
10
+ // writes it to the terminal tab; it never puts it on disk: none of thousands of
11
+ // transcript files measured carried a `summary` row, and main/agent-sessions.mjs
12
+ // found the same. So the name is not read from anywhere. It is written, here, by
13
+ // the same kind of call Claude Code makes: one small fast model, one message,
14
+ // one line back.
17
15
  //
18
- // AND IT IS WRITTEN FROM WHERE THE THREAD STANDS, NOT FROM ITS FIRST MESSAGE.
19
- // This is the difference she was shown and the one she picked. Titled from its
20
- // opening line alone came back "Fix onboarding and theme visibility", which was
21
- // true the day she wrote it and by then the row was about landing five
22
- // branches; given the last thing written on the row as well, the same call said
23
- // "Landing branches for app fixes". Her threads run for days, so a name fixed
24
- // at the first message goes stale the way rule 8 of her instructions is about,
25
- // and this is the same failure arriving from the other side.
16
+ // AND IT IS WRITTEN ONCE, FROM WHERE THE THREAD STOOD WHEN IT WAS NAMED.
17
+ // A thread's opening line describes where it started, so the prompt is given
18
+ // the last thing written on the row as well and the name describes the work
19
+ // rather than the first sentence of the ask. It used to be REWRITTEN every time
20
+ // the row's content moved, on the same reasoning, and that is what this file no
21
+ // longer does: a tester lost their threads because the names kept changing
22
+ // under them while agents worked (w-c141733b61, `wantsName` below). A name that
23
+ // follows the work is worth less than a name a person can find again.
26
24
  //
27
25
  // WHY THE APP DOES THIS AND NOT THE WORKER. It was written into briefs/worker.md
28
26
  // first and taken straight back out: the brief is pasted into every session and
29
27
  // tests/the-brief-only-says-what-applies.mjs holds it under 6,000 characters,
30
28
  // which it was 33 characters short of. A rule every worker must remember is also
31
- // a rule most of them will not, and the row she is looking at right now may have
29
+ // a rule most of them will not, and the row the user is looking at right now may have
32
30
  // no worker on it at all. Doing it here names every row on every product with
33
31
  // nothing for anyone to remember.
34
32
  //
35
- // The write goes to `label`, never to `title`. The title is hers, the fold makes
36
- // her outrank us on it, and she still reads her own words the moment she opens
37
- // the row (shared/work-items.mjs).
33
+ // The write goes to `label`, never to `title`. The title belongs to the user,
34
+ // the fold makes the user outrank us on it, and the user still reads their own
35
+ // words the moment they open the row (shared/work-items.mjs).
38
36
 
39
37
  import { spawn } from 'node:child_process';
38
+ import { codexLaunchEnv } from './codex-launch-env.mjs';
40
39
 
41
40
  /**
42
41
  * The model this runs on. Small and fast on purpose: this is a naming call, it
43
42
  * happens on a timer, and it must never be worth thinking about. Measured
44
- * through the CLI on 2026-09-03 over ten of her real rows: 14.1s median a call,
43
+ * through the CLI over ten real rows: 14.1s median a call,
45
44
  * nearly all of it the CLI starting a session rather than the model working. */
46
45
  export const NAME_MODEL = 'claude-haiku-4-5-20251001';
47
46
 
@@ -52,7 +51,7 @@ export const NAME_MODEL = 'claude-haiku-4-5-20251001';
52
51
  export const NAME_TIMEOUT_MS = 90_000;
53
52
 
54
53
  /**
55
- * What a name may be. Long enough for "Landing branches for app fixes", short
54
+ * What a name may be. Long enough for a seven-word name, short
56
55
  * enough that it can never be the thing that makes a row clip: the row draws
57
56
  * one line and the summary budget beside it is 112. */
58
57
  export const NAME_MAX = 64;
@@ -63,41 +62,72 @@ export const NAME_MAX = 64;
63
62
  const LIVE = new Set(['open', 'claimed', 'blocked']);
64
63
 
65
64
  /**
66
- * The newest moment anything about this row's CONTENT changed. Deliberately not
67
- * `updatedAt`: a heartbeat, a claim and a release all move that, and renaming a
68
- * row every time a worker touches it would spend a call a minute on a busy row
69
- * and change the words under her while she reads them. */
70
- export function contentTouchedAt(item) {
71
- const w = item?.wrote ?? {};
72
- let last = 0;
73
- for (const field of ['title', 'body', 'result', 'answer']) {
74
- const ts = w[field]?.ts ?? 0;
75
- if (ts > last) last = ts;
76
- }
77
- return last;
65
+ * PURE. Is this title already a name somebody wrote?
66
+ *
67
+ * The one question that decides whether this module speaks at all, and it is
68
+ * answered off the words alone because that is all there is: nothing on a row
69
+ * records whether its title was typed or derived.
70
+ *
71
+ * Three marks, and together they separate the two shapes `splitMessage` makes
72
+ * (renderer/src/message-split.ts). A first line of 120 characters or less is
73
+ * kept VERBATIM and is the name its person wrote. A longer one is clipped into
74
+ * a title by the app, and every clip it can produce is either a whole sentence
75
+ * of at least 25 characters or a hard cut ending in an ellipsis.
76
+ *
77
+ * as short as a name we would write NAME_MAX, so a title already that tight
78
+ * has nothing for us to improve,
79
+ * as few words as a name has the same eight `cleanName` allows,
80
+ * not ending as a sentence does '.', '!', '?' and '…' are how a clipped
81
+ * title ends and not how a name does.
82
+ *
83
+ * It errs towards leaving the words alone, which is the right way to be wrong:
84
+ * the cost is a short typed sentence keeping its own first line, and the cost
85
+ * the other way is somebody losing the thread they named.
86
+ */
87
+ export function isWrittenName(title) {
88
+ const t = String(title ?? '').trim();
89
+ if (!t) return false;
90
+ if (t.length > NAME_MAX) return false;
91
+ if (t.split(/\s+/).length > 8) return false;
92
+ return !/[.!?…]$/.test(t);
78
93
  }
79
94
 
80
95
  /**
81
96
  * PURE. Does this row want naming?
82
97
  *
83
- * Three ways to say no, and the third is the one that keeps this cheap:
98
+ * Four ways to say no, and the last two are the fix for a tester losing their
99
+ * own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
100
+ * header comment" and then "Update cart file comment" inside two minutes.
101
+ *
84
102
  * - it is done, so nobody is scanning for it,
85
103
  * - it has nothing to name from, which is a row with no words on it yet,
86
- * - its label is already at least as new as the newest thing said on the row.
104
+ * - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
105
+ * look for, and on a team it is the name they will point someone else at.
106
+ * Improving it is not an improvement.
107
+ * - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
108
+ *
109
+ * That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
110
+ * followed where the thread stood: an agent writes a note and a result on every
111
+ * run, so every run earned a new name. Following the thread was the intended
112
+ * behaviour and there was a test for it; what it cost in practice is that no
113
+ * thread stayed findable, so the brief for this change overrides both. The idea
114
+ * it was serving survives in the prompt, which still reads the latest thing on
115
+ * the row, so the one name a thread gets describes the work and not just its
116
+ * opening line.
87
117
  *
88
- * A row whose label we wrote and whose content has not moved since is finished
89
- * business forever, so a store that has settled costs nothing per tick.
118
+ * The exception is a name the person writes themselves later: a title newer
119
+ * than our label is a rename, so `rowTitle` draws theirs (list-rules.ts) and
120
+ * this stays quiet rather than answering it with another name of ours.
90
121
  */
91
122
  export function wantsName(item) {
92
123
  if (!item || !LIVE.has(item.status)) return false;
93
124
  if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
94
- const wroteAt = item.wrote?.label?.ts ?? 0;
95
- if (!String(item.label ?? '').trim()) return true;
96
- return wroteAt < contentTouchedAt(item);
125
+ if (isWrittenName(item.title)) return false;
126
+ return !String(item.label ?? '').trim();
97
127
  }
98
128
 
99
129
  /**
100
- * PURE. The one message the naming call gets. Her words first, then the last
130
+ * PURE. The one message the naming call gets. The user's words first, then the last
101
131
  * thing written on the row, which is what makes the name describe the thread
102
132
  * rather than its opening line. Truncated hard: a name does not get better for
103
133
  * having read six thousand words, and this is spawned on a timer. */
@@ -148,21 +178,55 @@ export function cleanName(text) {
148
178
  return bare;
149
179
  }
150
180
 
181
+ /**
182
+ * THE SAME CALL ON A MAC THAT HAS ONLY CODEX. `codex exec` prints the reply
183
+ * alone on stdout and everything else on stderr, so the reading below is the
184
+ * same. No session is kept, none of her Codex config or MCP servers is loaded,
185
+ * and nothing may be written. Measured 2026-10-01, codex-cli 0.160.0: 5.6s. */
186
+ export function codexNameArgs(item) {
187
+ return codexArgs(namePrompt(item));
188
+ }
189
+
190
+ function codexArgs(prompt) {
191
+ return [
192
+ 'exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
193
+ '-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt,
194
+ ];
195
+ }
196
+
151
197
  /**
152
198
  * Name one row. Resolves to a clean name, or to '' for every failure there is:
153
199
  * the binary missing, a non-zero exit, a timeout, an empty reply, a reply that
154
200
  * is not a name. Never throws and never rejects, because this is called from a
155
201
  * timer whose only correct behaviour on failure is to leave the row alone.
156
202
  */
157
- export function nameRow(item, { claudeBin, model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS } = {}) {
203
+ export function nameRow(item, opts = {}) {
204
+ return askSmallModel(namePrompt(item), cleanName, opts);
205
+ }
206
+
207
+ /**
208
+ * One short question to the small model, on Claude Code or Codex, its reply put
209
+ * through `clean`. The same promise as `nameRow` and for the same reason: ''
210
+ * for every failure, never a throw. Shared with the message sorter
211
+ * (main/message-priority.mjs).
212
+ *
213
+ * `env` is which account the call bills. Without one it inherits the app's own
214
+ * environment, which is always the default Claude account: when that account
215
+ * hit its weekly limit on 2026-10-06 every call died in two seconds and no row
216
+ * was named for a day, while agents ran on the second account. The supervisor
217
+ * passes one per account and walks them (`_askSmall` in main/supervisor.mjs).
218
+ */
219
+ export function askSmallModel(prompt, clean, { claudeBin, codexBin = null, engine = 'claude', model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS, env } = {}) {
158
220
  return new Promise((resolve) => {
159
- if (!claudeBin) return resolve('');
221
+ const bin = engine === 'codex' ? codexBin : claudeBin;
222
+ if (!bin) return resolve('');
160
223
  let child;
161
224
  try {
162
- child = spawn(claudeBin, ['-p', namePrompt(item), '--model', model], {
225
+ child = spawn(bin, engine === 'codex' ? codexArgs(prompt) : ['-p', prompt, '--model', model], {
163
226
  // Somewhere that is nobody's project. A naming call must not pick up a
164
227
  // CLAUDE.md, a settings file or a hook from whatever folder it lands in.
165
228
  cwd: '/tmp',
229
+ ...(engine === 'codex' ? { env: codexLaunchEnv(bin, env) } : env ? { env } : {}),
166
230
  stdio: ['ignore', 'pipe', 'pipe'],
167
231
  });
168
232
  } catch {
@@ -178,8 +242,8 @@ export function nameRow(item, { claudeBin, model = NAME_MODEL, timeoutMs = NAME_
178
242
  resolve(value);
179
243
  };
180
244
  const timer = setTimeout(() => finish(''), timeoutMs);
181
- child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(cleanName(out)); });
245
+ child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(clean(out)); });
182
246
  child.on('error', () => finish(''));
183
- child.on('close', (code) => finish(code === 0 ? cleanName(out) : ''));
247
+ child.on('close', (code) => finish(code === 0 ? clean(out) : ''));
184
248
  });
185
249
  }
@@ -0,0 +1,93 @@
1
+ // WHICH ACCOUNT THE AGENTS ARE ACTUALLY RUNNING ON, read off this Mac.
2
+ //
3
+ // w-e217e577e5, 2026-10-07, from a session with a new user: "He didn't realize
4
+ // it auto-connected to Claude/Codex; he wasn't sure how it was even running."
5
+ //
6
+ // Every fact here was already on the disk and already being read, each by the
7
+ // screen that needed it: main/claude-plan.mjs for the Claude plan (the Accounts
8
+ // page), main/codex-account.mjs for the ChatGPT one (the Codex card). What did
9
+ // not exist was ONE answer to "what is powering this", in a shape small enough
10
+ // to ride the snapshot and be drawn in a corner. That is all this is: the two
11
+ // readers, asked about the one account the fleet is spending, and null whenever
12
+ // the answer would be a guess.
13
+ //
14
+ // NULL IS AN ORDINARY ANSWER AND IT MEANS DRAW NOTHING. A Mac mid-setup, a
15
+ // config file being rewritten under us, a login that exists with no plan string
16
+ // in it: the first two are null, the third is the name with no plan. Nothing
17
+ // here ever reports an account that is not there, because the corner it feeds
18
+ // is read by somebody deciding whose money this is spending.
19
+ //
20
+ // IT IS CACHED ON THE LOGIN FILES' OWN TIMES, NOT ON A CLOCK. The snapshot is
21
+ // built every ten seconds and `~/.claude.json` is 148 KB on the machine this was
22
+ // written on, so parsing it per tick is work nobody asked for; it is also a file
23
+ // that genuinely changes, since Claude Code refetches its own profile as it
24
+ // runs. `signInFiles`/`signInStamp` (main/sign-in-files.mjs) already name the
25
+ // files a login writes and read their modification times and nothing else, so
26
+ // the cache key is the newest of those times. A moved time re-reads. A cache
27
+ // that aged out on a timer instead would be either stale after a sign-in or
28
+ // re-parsing for nothing, and the file itself knows which.
29
+
30
+ import fs from 'node:fs';
31
+ import os from 'node:os';
32
+ import path from 'node:path';
33
+ import { planFile, planOfAccount } from './claude-plan.mjs';
34
+ import { codexAccount, codexAuthFile } from './codex-account.mjs';
35
+ import { signInFiles, signInStamp } from './sign-in-files.mjs';
36
+
37
+ /** keyed by engine and folder, valued by the stamp it was read at. */
38
+ const remembered = new Map();
39
+
40
+ /**
41
+ * WHAT THE FLEET IS RUNNING ON, or null because nothing readable is signed in.
42
+ *
43
+ * @param {object} facts
44
+ * @param {string} facts.engine Which coding agent the workspace runs on, which
45
+ * the supervisor has already decided (`Supervisor#engineFacts`). Never worked
46
+ * out here: the capability gate lives there, and a corner that guessed could
47
+ * name the subscription the fleet is not spending.
48
+ * @param {string} [facts.claudeProfile] The Claude login, `default` or a folder.
49
+ * @param {string} [facts.codexHome] The CODEX_HOME this workspace runs on.
50
+ * @returns {{ engine: string, plan: string|null }|null} `plan` is the words the
51
+ * tool itself reports, never a label of ours.
52
+ */
53
+ export function runsOn({ engine = null, home = os.homedir(), claudeProfile = 'default', codexHome = null } = {}) {
54
+ if (engine !== 'claude' && engine !== 'codex') return null;
55
+ const folder = engine === 'codex'
56
+ ? (codexHome || path.join(home, '.codex'))
57
+ : (!claudeProfile || claudeProfile === 'default' ? null : claudeProfile);
58
+ // The home is in the key as well as the folder. A default Claude login has no
59
+ // folder of its own, so without it two homes share one entry, which is exactly
60
+ // what a test with a throwaway home does and what a packaged app would do if
61
+ // HOME ever moved under it.
62
+ const key = `${engine}:${home}:${folder ?? 'default'}`;
63
+ const stamp = signInStamp(signInFiles({ engine, folder, home }));
64
+ const seen = remembered.get(key);
65
+ if (seen && seen.stamp === stamp) return seen.value;
66
+ const value = stamp ? read(engine, { home, claudeProfile, folder }) : null;
67
+ remembered.set(key, { stamp, value });
68
+ return value;
69
+ }
70
+
71
+ function read(engine, { home, claudeProfile, folder }) {
72
+ if (engine === 'codex') {
73
+ const id = codexAccount(null, { file: codexAuthFile(folder) });
74
+ return id ? { engine: 'codex', plan: id.plan ?? null } : null;
75
+ }
76
+ // THE LOGIN AND THE PLAN COME OUT OF ONE READ. `readPlan` answers only the
77
+ // second, and "not signed in" and "signed in with no plan string" are two
78
+ // different screens: the first draws nothing, the second draws the name.
79
+ let account = null;
80
+ try {
81
+ account = JSON.parse(fs.readFileSync(planFile(claudeProfile, home), 'utf8'))?.oauthAccount ?? null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ if (!account || typeof account !== 'object') return null;
86
+ const plan = planOfAccount(account);
87
+ return { engine: 'claude', plan: plan.known ? plan.label : null };
88
+ }
89
+
90
+ /** Forgets every cached reading. For tests, and for a sign-out that lands. */
91
+ export function forgetRunsOn() {
92
+ remembered.clear();
93
+ }
package/main/serve.mjs CHANGED
@@ -28,10 +28,17 @@ import path from 'node:path';
28
28
  import crypto from 'node:crypto';
29
29
  import { fileURLToPath } from 'node:url';
30
30
 
31
+ import { spawn } from 'node:child_process';
31
32
  import { loadConfig } from './config.mjs';
32
33
  import { Store } from './store.mjs';
33
34
  import { Supervisor } from './supervisor.mjs';
35
+ import { carryHerBriefsAcross, joinMessageRules, setAsideShippedMessageRules, unifyAgentInstructions } from './instruction-settings.mjs';
34
36
  import { registerIpc } from './ipc.mjs';
37
+ import { reportFromRenderer } from './crash-report.mjs';
38
+ import * as workItemsDisk from './store/work-items.mjs';
39
+ import { createTeamService, teamStateFile } from './team/index.mjs';
40
+ import { loadCloudConfig, supabaseSession, headlessSessionFile } from './team/session.mjs';
41
+ import { appHome, carryMisplacedMachinery, storeRootEnv } from './store/home.mjs';
35
42
 
36
43
  const here = path.dirname(fileURLToPath(import.meta.url));
37
44
  const repoRoot = path.dirname(here);
@@ -114,6 +121,28 @@ function readVersion() {
114
121
  }
115
122
  }
116
123
 
124
+ /**
125
+ * The four channels main/main.mjs registers beside its window, not in ipc.mjs.
126
+ *
127
+ * Each is about the desktop window itself (the dock badge, a ⌘R, banners in
128
+ * Notification Center, the window's own crashes), so `registerIpc` never
129
+ * files them and a tab asking got a 404. That was not harmless: a failed
130
+ * `badge` became an uncaught error, the screen reported it on `crash`, that
131
+ * failed too, and the tab sent thousands of requests a second.
132
+ * tests/a-browser-tab-gets-an-answer-on-every-channel.test.mjs.
133
+ *
134
+ * A tab has no dock and no ⌘R of ours to report, and nothing here decides
135
+ * banners yet, so three of them answer with nothing. A crash goes through the
136
+ * same scrubbed path as the desktop's, which writes nothing until crash
137
+ * reports are set up for this process.
138
+ */
139
+ function answerWhatOnlyTheWindowAnswered(ipcMain) {
140
+ ipcMain.handle('zero:badge', () => null);
141
+ ipcMain.handle('zero:notify', () => null);
142
+ ipcMain.handle('zero:boot-info', () => ({ reloaded: false, builtAt: null, recovered: null }));
143
+ ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); return null; });
144
+ }
145
+
117
146
  /**
118
147
  * Boot the doing half, with no Electron anywhere.
119
148
  *
@@ -124,12 +153,48 @@ function readVersion() {
124
153
  */
125
154
  export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, userDir = dataDir } = {}) {
126
155
  const config = loadConfig(dataDir);
156
+ // The browser editor uses the same user files as the desktop. Finish the
157
+ // older migrations first so only user content enters the combined file.
158
+ carryHerBriefsAcross(appDir, userDir);
159
+ joinMessageRules(appDir, userDir);
160
+ setAsideShippedMessageRules(appDir, userDir);
161
+ unifyAgentInstructions(appDir, userDir);
162
+ // THE APP'S OWN HOME IS THE STORE ROOT, BEFORE ANYTHING OPENS THE STORE, as
163
+ // main/main.mjs does for the desktop. The ledgers live under the home the
164
+ // store reads from this variable, and every worker is handed `storeRoot` as
165
+ // that same variable. Without this line the two disagreed: the copy wrote its
166
+ // rows wherever an inherited variable pointed (or a dot-folder in $HOME), each worker
167
+ // looked under `storeRoot`, and every store call answered "no work item".
168
+ // tests/an-agent-in-a-browser-copy-can-reach-its-own-work-item.test.mjs.
169
+ // Rows already written under the old home come along, or they would vanish.
170
+ const inheritedHome = appHome();
171
+ Object.assign(process.env, storeRootEnv(config.storeRoot));
172
+ carryMisplacedMachinery(inheritedHome, config.storeRoot);
127
173
  const store = await new Store(config).init();
128
174
  const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
129
175
  const window = broadcastingWindow();
130
176
  const host = nodeHost();
131
- const ipc = registerIpc({ store, supervisor, config, window, host });
132
- return { config, store, supervisor, window, host, ipc, channels: host.ipcMain.handlers };
177
+ // THE TEAM, as on the desktop (main/main.mjs), with no Electron: the session
178
+ // file is plain JSON readable only by this user, kept in this copy's own
179
+ // folder and not in the store (headlessSessionFile), and Google sign-in opens
180
+ // in the Mac's own browser.
181
+ const cloudConfig = loadCloudConfig(appDir);
182
+ let ipc = null;
183
+ const team = cloudConfig ? createTeamService({
184
+ session: supabaseSession({
185
+ cloudConfig,
186
+ sessionFile: headlessSessionFile({ userDir, storeRoot: config.storeRoot }),
187
+ openExternal: async (url) => { spawn('open', [url], { stdio: 'ignore', detached: true }).unref(); },
188
+ }),
189
+ store,
190
+ disk: workItemsDisk,
191
+ accountRoot: config.accountRoot,
192
+ stateFile: teamStateFile(config.storeRoot),
193
+ onChange: () => ipc?.push?.(),
194
+ }) : null;
195
+ ipc = registerIpc({ store, supervisor, config, window, host, team });
196
+ answerWhatOnlyTheWindowAnswered(host.ipcMain);
197
+ return { config, store, supervisor, window, host, ipc, team, channels: host.ipcMain.handlers };
133
198
  }
134
199
 
135
200
  /** A token in the url, because the port alone is not a door key. */
@@ -161,10 +226,17 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
161
226
  return http.createServer(async (req, res) => {
162
227
  const url = new URL(req.url, 'http://127.0.0.1');
163
228
 
164
- // The token rides in a header on api calls and in the query on the first
165
- // page load, because a browser cannot put a header on a navigation.
229
+ // The token rides in a header on api calls and in the query on the event
230
+ // stream, because a browser cannot put a header on either a navigation or
231
+ // an EventSource. It guards the two doors that reach the store. The built
232
+ // screen is the same public files for everybody, and its own script and
233
+ // style requests carry no token, so asking for one there drew a blank tab.
234
+ // NOR DOES THE PAGE ITSELF ASK: the tab takes the token out of its address
235
+ // bar on load, so a reload asks for `/` bare, and refusing that drew
236
+ // "Wrong or missing token" on every reload (tests/a-browser-tab-survives-a-reload).
237
+ const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/');
166
238
  const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
167
- if (given !== token) {
239
+ if (guarded && given !== token) {
168
240
  res.writeHead(403, { 'content-type': 'text/plain' });
169
241
  res.end('Wrong or missing token. Use the url the terminal printed.');
170
242
  return;