agentbox-app 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -1,65 +1,23 @@
1
1
  import { HARNESS_DEFINITIONS } from './harness-definitions.mjs';
2
- // WHICH CODING AGENT A TASK IS HANDED TO.
2
+ // Which coding agent a task is handed to.
3
3
  //
4
- // AND THE PREMISE SHE GAVE FOR IT DID NOT HOLD. Measured head to head on her
5
- // Mac the same day, identical task, four fresh repos each: Codex 16.8 / 45.3 /
6
- // 96.9 / 18.6 seconds, Claude Code 19.0 / 19.8 / 19.8 / 21.2. Both correct
7
- // every run. Codex is NOT faster; it is slower on median and it wanders. That
8
- // is written here rather than in a report because it is the reason this file
9
- // must never make Codex the default for anybody, including her. Claude Code is
10
- // the default and stays the default until she says otherwise. What this file
11
- // buys is a SECOND ENGINE on a second subscription, which is the thing that
12
- // actually went wrong for her, and a choice she makes per task.
4
+ // Claude Code is the default. Codex measured slower on median and wandered on
5
+ // the same tasks, so do not make it the default for anybody.
13
6
  //
14
- // THIS FILE WAS DELETED AND IS COMING BACK IN STAGES, WHICH IS WHY IT NOW HAS
15
- // A GATE. Her own message on that commit says thirteen; the number here is the
16
- // one the tree can be asked for.
7
+ // Old rows may still carry `engine: "codex"`, because the work-item fold keeps
8
+ // every field it was given. So `engineFor` answers Claude Code unless the
9
+ // caller hands it `ENGINE_CHOICE_ENABLED`, and a caller that passes nothing
10
+ // gets the safe answer.
17
11
  //
18
- // `engine` DID NOT GO. It is still a live name on the work-item contract
19
- // (`WORK_ITEM_FIELDS`, shared/work-items.mjs:95, accepted by the patch
20
- // validator at :290, stored as a free string and validated against nothing,
21
- // because "nothing here reads them"). The fold has no migration and drops
22
- // nothing, so a row marked between 08-25 and 08-27 still carries
23
- // `engine: "codex"` UNLESS a later line overwrote that one field — which is
24
- // possible: the fold lets an equal-or-stronger write replace a field held by
25
- // an earlier one (`beats`, the authority comparison near shared/work-items.mjs
26
- // :494). Nothing has ever been written to clear it on purpose, so the honest
27
- // statement is "probably still there", not "always will be".
28
- //
29
- // HOW MANY SUCH ROWS A STORE ACTUALLY HAS IS AN OPEN QUESTION, AND IT IS NOT
30
- // ZERO UNTIL SOMEBODY LOOKS. It could not be measured from the machine this was
31
- // written on: there is no ~/Zero there at all. On a real store it is one command,
32
- // `grep -rho '"engine":"[a-z]*"' ~/Zero | sort | uniq -c`, and the answer
33
- // belongs in this paragraph when someone runs it. The gate below costs nothing
34
- // if the count turns out to be zero, and is the whole story if it is not.
35
- //
36
- // So the 08-25 resolver, restored unchanged, would have been a silent reversal
37
- // of the decision to withhold Codex: those rows would start choosing Codex again the moment this
38
- // module landed, with no picker, nothing in Settings, and no approval path
39
- // built. The resolver is therefore CLOSED unless the caller hands it
40
- // `ENGINE_CHOICE_ENABLED` below. A caller that passes nothing gets Claude
41
- // Code, because the safe answer is the one that has to be the accident.
42
- //
43
- // This module holds the names and the one rule. Everything Codex-shaped —
44
- // arguments, its event stream, where it writes its transcript — belongs in
45
- // main/codex.mjs when that comes back, because the renderer must never import
46
- // spawn code.
12
+ // This module holds the names and the one rule. Codex arguments and its event
13
+ // stream live in main/codex.mjs, because the renderer imports this file and
14
+ // must never import spawn code.
47
15
 
48
16
  /**
49
- * The engines, in the order the picker draws them.
50
- *
51
- * ADMITTED ONES ONLY, and that word is doing real work as of 2026-10-07. A
52
- * harness being integrated and a harness being offered are two different
53
- * decisions, and this list is the second one. OpenCode is registered in
54
- * main/harnesses.mjs, its transport is measured and tested, and it is
55
- * deliberately absent from here -- so `ENGINE_IDS`, `isEngine`, `engineFor`,
56
- * the composer's picker, the Settings row and the byline all behave as if it
57
- * were not there, while `harnessFor('opencode')` works for anything that asks
58
- * for it by name. Without this filter, adding the definition alone would have
59
- * put a third word in front of every user the moment it landed.
60
- *
61
- * docs/harnesses.md holds the bar a harness clears before `admitted` goes
62
- * true, and the product review that turning it on requires.
17
+ * The engines, in the order the picker draws them. Admitted harnesses only: a
18
+ * registered harness (OpenCode, say) still works through `harnessFor` by name
19
+ * but is not offered anywhere until it is admitted. The bar for admission is in
20
+ * docs/harnesses.md.
63
21
  */
64
22
  export const ENGINES = HARNESS_DEFINITIONS
65
23
  .filter(({ admitted }) => admitted !== false)
@@ -68,164 +26,58 @@ export const ENGINES = HARNESS_DEFINITIONS
68
26
  export const ENGINE_IDS = ENGINES.map((e) => e.id);
69
27
 
70
28
  /**
71
- * What a task runs on when nobody has chosen.
72
- *
73
- * Not a setting and not a coin flip. See the measurement above: Claude Code is
74
- * faster on median and never wandered, and it is the only one of the two that
75
- * is WIRED to stop and ask her for permission mid-run.
76
- *
77
- * That second clause used to say Codex CANNOT ask, and it is now wrong twice
78
- * over. It was true of `codex exec`, which is what the removed 08-25 build
79
- * drove and what the sentence was written about. `codex app-server` raises real
80
- * approval requests, measured 2026-09-04, main/codex-app-server.mjs answers
81
- * them, main/codex-approvals.mjs draws them as the card she already knows, and
82
- * the spool they land in is the one the other engine uses. This paragraph went
83
- * on listing all four of those as missing for a whole slice after two of them
84
- * had been built.
85
- *
86
- * THE PICKER IS BUILT NOW TOO, 2026-09-04: a word in the composer's sentence
87
- * ("With Codex."), a row in Settings, and the engine named in the byline --
88
- * every one of them drawn only where `Supervisor#engineChoices` says this Mac
89
- * really offers a choice, which means the gate is open and the binary is there.
90
- * That paragraph used to say the missing picker was why the default had not
91
- * moved, and it was the last of four such claims to stop being true.
92
- *
93
- * SO THE DEFAULT IS HELD BY THE MEASUREMENT AT THE TOP OF THIS FILE AND BY
94
- * NOTHING ELSE. Codex is slower on median and it wanders; Claude Code stays the
95
- * default until she says otherwise, and now that there is a picker that is a
96
- * decision rather than a consequence.
29
+ * What a task runs on when nobody has chosen. It is Claude Code for the speed
30
+ * reason at the top of this file, not because Codex lacks approvals or a
31
+ * picker: both exist now.
97
32
  */
98
33
  export const DEFAULT_ENGINE = 'claude';
99
34
 
100
35
  /**
101
- * THE CAPABILITY GATE: an explicit code opt-in, saying that whoever wrote this
102
- * call site knew there was a second engine and meant to allow it.
103
- *
104
- * BE PRECISE ABOUT WHAT IT PROVES, because a gate believed to be stronger than
105
- * it is, is worse than no gate. It is a hard barrier against SERIALIZED DATA:
106
- * nothing that arrives as a ledger line, a config file or an IPC message can
107
- * open it, ever. It is NOT proof that the approval path she needs was actually
108
- * built, and it is not a defence against our own code — any module in this repo
109
- * can import the token. It moves the decision from data to source, and that is
110
- * all it does. It is the second half, the picker and her consent, that makes
111
- * the choice real.
112
- *
113
- * It is a Symbol, and that shape is the whole design:
114
- *
115
- * - A SYMBOL CANNOT ARRIVE FROM OUTSIDE THE PROCESS. Every input that could
116
- * carry a stale `codex` is serialized: the ledger line off disk and
117
- * `zero.config.json` are JSON, which has no way to spell a Symbol and drops
118
- * it in silence, and an Electron IPC payload goes through the structured
119
- * clone algorithm, which does not drop a Symbol but THROWS DataCloneError on
120
- * one. Neither can express the gate. The louder of the two failures is the
121
- * better one and we get it where it matters least; the point is that the
122
- * untrusted data and the permission live in different type systems.
123
- * - IT IS `Symbol`, NEVER `Symbol.for`. The global registry would let any code
124
- * anywhere mint an equal one out of a string. This one exists only where
125
- * somebody imported this module and named it.
126
- * - IT IS NOT A BOOLEAN. `enabled: true` is the kind of thing a caller spreads
127
- * in by accident from some other options object. This is not.
36
+ * The capability gate: passing this to `engineFor` says the call site meant to
37
+ * allow a second engine. It blocks serialized data only. Any module here can
38
+ * import it.
39
+ *
40
+ * It is a Symbol because nothing from outside the process can carry one: JSON
41
+ * drops it, and Electron's structured clone throws DataCloneError on it. It
42
+ * uses `Symbol`, not `Symbol.for`, so no string can mint an equal one, and it
43
+ * is not a boolean, so a spread options object cannot open it by accident.
128
44
  */
129
45
  export const ENGINE_CHOICE_ENABLED = Symbol('the second engine is wired up in this build');
130
46
 
131
- /**
132
- * WHEN SHE OPENED THE GATE, read off `zero.config.json`, or null. This is the
133
- * one value in the app that can cause the Symbol above to be handed over
134
- * (main/supervisor.mjs `_engineFor`, the only holder of it).
135
- *
136
- * THE OPT-IN IS A MOMENT AND NOT A FLAG, and that is the whole design of it.
137
- *
138
- * `engine` survived the 2026-08-27 removal on the work-item contract and the
139
- * fold drops nothing, so her store may still hold rows marked `codex` between
140
- * 08-25 and 08-27. How many is unknown; on her Mac it is one command,
141
- * `grep -rho '"engine":"[a-z]*"' ~/Zero | sort | uniq -c`, and the answer
142
- * belongs here when somebody runs it. A boolean opt-in would mean "every row
143
- * that ever said codex is live from now on", and the first thing it would do is
144
- * route an unknown number of those onto a second engine she had marked before
145
- * there was any way to choose. That is the exact hazard the gate was built for,
146
- * so an opt-in that walks into it is not one worth having. The picker built on
147
- * 2026-09-04 does not soften this: an August row is still not a choice she is
148
- * making now, and the byline draws it as the engine that will really run it.
149
- *
150
- * THOSE TWO MEANINGS ARE SEPARABLE, AND THE LEDGER ALREADY HOLDS WHAT
151
- * SEPARATES THEM. The fold records `wrote[field] = { ts, source }` per field
152
- * and stopped deleting it on the way out precisely because readers needed it
153
- * (shared/work-items.mjs:324; shared/answers.mjs reads `wrote.answer.ts` the
154
- * same way). So "Codex may be chosen FROM NOW ON" is a checkable statement
155
- * rather than a hope, and `engineChoiceOnRowIsStale` below is the check.
156
- *
157
- * WHY NOT A BOOLEAN PLUS A REMEMBERED "first seen at". Because that would live
158
- * in the app's own state rather than in her file: a fresh install, a state file
159
- * she deleted, or a second Mac would each invent a different moment, and the
160
- * one thing this value must be is the same on every machine reading the same
161
- * store. A date the user typed is a fact about their decision; a date we noticed is a
162
- * fact about a process.
163
- *
164
- * THE WORKSPACE DEFAULT (`config.engine`) IS TIME-SCOPED TOO, SINCE 2026-09-05,
165
- * AND THIS PARAGRAPH USED TO SAY IT COULD NOT BE. Its words were: "JSON has no
166
- * per-key write times, and `engine` sits in the same file as this, so opening
167
- * that file to write the moment is the same act as reading the line above it."
168
- * The first clause is true of JSON and irrelevant: the APP has write times, one
169
- * module writes that key (`setWorkspaceSetting`, main/settings.mjs), and
170
- * `saveConfig` takes a patch of several keys, so `engineAt` rides in the same
171
- * write. Without it, closing the gate and reopening it with a NEWER moment
172
- * re-armed a workspace default set weeks earlier -- every row that names
173
- * nothing went straight back to Codex without her saying so again, which is the
174
- * exact claim this value is supposed to make impossible, and the rollback
175
- * lifecycle is the thing the gate is FOR.
176
- *
177
- * AND THE SECOND CLAUSE, READ CORRECTLY, IS WHY AN ABSENT STAMP MEANS HONOURED.
178
- * `engineChoice` is hand-written into this file; a hand-written `engine` on the
179
- * line above it was put there by the same hand in the same act, so the two ARE
180
- * contemporaneous and there is nothing to be stale against. Only a default the
181
- * app wrote can carry a stamp, and only a stamped default can go stale — which
182
- * is also what keeps every config that exists today behaving exactly as it did.
183
- * `engineDefaultIsStale` below is the check.
184
- *
185
- * IT IS PARSED STRICTLY, WHICH IS NOT THE SAME AS PARSED. `Date.parse` reads
186
- * "1", "2026", "Sep 4 2026" and "2026/09/04" as real dates, so a bare parse
187
- * would let `"engineChoice": "1"` open the second engine. The shape is checked
188
- * first and everything else is off, because the safe answer is the one that has
189
- * to be the accident.
190
- */
47
+ // The shape of a date somebody wrote down. `Date.parse` alone would accept "1"
48
+ // or "2026", and `"engineChoice": "1"` must not open the second engine.
191
49
  const A_MOMENT = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
192
50
 
193
- /**
194
- * One strict parse, used by both moments in this file, so the two cannot come
195
- * to disagree about what counts as a date somebody wrote down. */
196
51
  function momentIn(written) {
197
52
  if (typeof written !== 'string' || !A_MOMENT.test(written)) return null;
198
53
  const at = Date.parse(written);
199
54
  return Number.isFinite(at) ? at : null;
200
55
  }
201
56
 
57
+ /**
58
+ * When the user opened the gate (`engineChoice` in zero.config.json), or null.
59
+ * It is a moment, not a flag, so an engine written on a row or as the default
60
+ * before it is not a choice made now and is ignored. It lives in the config
61
+ * rather than app state so every machine reading the same store agrees.
62
+ * main/supervisor.mjs `_engineFor` is the only place that turns it into the token.
63
+ */
202
64
  export function engineChoiceSince(config = {}) {
203
65
  return momentIn(config?.engineChoice);
204
66
  }
205
67
 
206
68
  /**
207
- * WHEN THE WORKSPACE DEFAULT WAS SET, read off `zero.config.json`, or null.
208
- *
209
- * Null means no stamp, and no stamp means a hand-written default — see the long
210
- * note above `engineChoiceSince`. It is the same strict parse for the same
211
- * reason: a claim that cannot be read is not a claim, so it reads as no stamp,
212
- * which is the hand-written case.
69
+ * When the app last wrote the workspace default (`engineAt`, written beside
70
+ * `engine` by `setWorkspaceSetting`), or null. Null means the default was typed
71
+ * into the file by hand, in the same act as `engineChoice`, so it is never stale.
213
72
  */
214
73
  export function engineDefaultSince(config = {}) {
215
74
  return momentIn(config?.engineAt);
216
75
  }
217
76
 
218
77
  /**
219
- * Whether the workspace default is one she set BEFORE the gate's current
220
- * moment: a setting from the build before the rollback, rather than a decision
221
- * she is making now.
222
- *
223
- * False when nobody has opted in, because there is no moment to be stale
224
- * against. False when the default is Claude Code, which asks for nothing the
225
- * gate withholds — the same rule `engineChoiceOnRowIsStale` follows below, and
226
- * for the same reason: discarding it could only ever move work TO the second
227
- * engine, which is the one direction the gate exists to prevent. False when
228
- * there is no stamp, which is a default typed beside the moment itself.
78
+ * Whether the workspace default was set before the gate's current moment.
79
+ * Never true for Claude Code: discarding that default could only move work onto
80
+ * the second engine.
229
81
  */
230
82
  export function engineDefaultIsStale(config = {}) {
231
83
  const since = engineChoiceSince(config);
@@ -236,33 +88,13 @@ export function engineDefaultIsStale(config = {}) {
236
88
  }
237
89
 
238
90
  /**
239
- * Whether the engine named on this row is one picked BEFORE she opened the
240
- * gate: an August row, rather than a decision she is making now.
241
- *
242
- * False when nobody has opted in, because there is no moment to be stale
243
- * against and the gate itself is what refuses then. False when the row names no
244
- * engine, or names one nothing recognises, because neither is asking for
245
- * anything and a warning on an ordinary row is noise on every row. True when
246
- * the row names an engine and there is no `wrote.engine.ts` at or after the
247
- * moment — which also covers an imported session and an old line with no
248
- * `wrote` map at all, where there is simply no evidence she chose anything.
249
- *
250
- * AND FALSE FOR A ROW THAT NAMES CLAUDE CODE, WHICH THIS ASKED WITH `isEngine`
251
- * AND GOT BACKWARDS. A row saying `engine: "claude"`, written before the moment,
252
- * was discarded as stale — and `engineFor` then fell through to `config.engine`,
253
- * which may be `codex`. So her explicit "run this on Claude Code" was read as no
254
- * choice at all and answered with the second engine, which is the precise
255
- * opposite of what staleness is for. `enginePicked` is the fix and it is the
256
- * whole of it: naming the DEFAULT asks for nothing the gate withholds, so there
257
- * is nothing to be stale about, and discarding it could only ever move work onto
258
- * the engine the gate was built to hold back.
91
+ * Whether the engine named on this row was written before the gate's moment.
92
+ * A row with no `wrote.engine.ts` counts as stale, since there is no evidence
93
+ * of a choice. A row naming Claude Code is never stale: discarding it would let
94
+ * `engineFor` fall through to a Codex workspace default.
259
95
  *
260
- * `wrote.engine.source` is deliberately NOT consulted. would be the stronger
261
- * rule, but CLAUDE.md is explicit that a ledger line names its own author as a
262
- * plain string and nothing checks it, so a source check would guard against an
263
- * accident and never against a hostile worker, while silently refusing whatever
264
- * a future picker happens to write. Who may write `engine` at all belongs to
265
- * the slice that draws one.
96
+ * `wrote.engine.source` is not consulted, because a ledger line names its own
97
+ * author as an unchecked string.
266
98
  */
267
99
  export function engineChoiceOnRowIsStale(item, config = {}) {
268
100
  const since = engineChoiceSince(config);
@@ -281,34 +113,14 @@ export function engineLabel(id) {
281
113
  }
282
114
 
283
115
  /**
284
- * Which engines this Mac can actually run, given what the finders found.
285
- *
286
- * `found` is a map of id to boolean. An engine whose binary is not on the
287
- * machine is not offered, because a picker that offers something that cannot
288
- * run is a picker that hands her a session that dies on arrival. Claude Code
289
- * is in the list even when it was not found, UNLESS Codex was: a Mac with only
290
- * Codex is offered only Codex (`homeEngine` below). With neither, the app
291
- * refuses to open an inbox and says so in its own words.
292
- *
293
- * UNGATED, AND THE INVARIANT IS THEREFORE SCOPED TO ROUTING. Say it plainly,
294
- * because the file must not claim more than the code delivers: this function,
295
- * `engineChoiceExists` and `enginePicked` will all happily describe a two-engine
296
- * Mac without anyone holding the token. A future renderer could draw a picker
297
- * out of them, and the gate would not stop it — she would be offered a choice
298
- * that `engineFor` then refuses, which is its own kind of lie.
299
- *
300
- * That is a deliberate trade and not an oversight. These three answer "what is
301
- * on this Mac" and "what did she pick"; they spawn nothing, and requiring a
302
- * capability token to answer a question about the filesystem buys nothing real.
303
- *
304
- * AND THE SLICE THAT DREW A PICKER ANSWERED FOR IT, 2026-09-04. There is
305
- * exactly one caller, `Supervisor#engineChoices`, and it asks
306
- * `engineChoiceSince` FIRST -- the same expression `_engineFor` derives the gate
307
- * token from -- so the offer and the routing are refused by the same fact and
308
- * cannot come apart. A Mac with Codex installed and no moment written answers
309
- * one engine here and Claude Code there. The renderer calls none of these: it
310
- * is handed the list on the snapshot. tests/a-codex-row-from-august-still-runs-on-claude.test.mjs
311
- * holds the caller list exactly, so the next one has to argue for itself here.
116
+ * Which engines this Mac can run, given `found` (id to boolean from the
117
+ * finders). Claude Code is listed even when not found, unless Codex was.
118
+ *
119
+ * This is ungated, like `engineChoiceExists` and `enginePicked`. Its one caller
120
+ * is `Supervisor#engineChoices`, which asks `engineChoiceSince` first, so the
121
+ * offer and the routing cannot disagree. The renderer gets the list on the
122
+ * snapshot. tests/a-codex-row-from-august-still-runs-on-claude.test.mjs pins the
123
+ * caller list.
312
124
  */
313
125
  export function availableEngines(found = {}) {
314
126
  const home = homeEngine(found);
@@ -316,13 +128,9 @@ export function availableEngines(found = {}) {
316
128
  }
317
129
 
318
130
  /**
319
- * WHAT RUNS WHEN NOBODY HAS CHOSEN, ON THIS MAC. Claude Code, unless this Mac
320
- * has said for certain that Claude Code is not here and Codex is. The app needs
321
- * one of the two, not both, so a Mac with only Codex runs everything on Codex.
322
- *
323
- * `found.claude` has to be an explicit `false`. A caller that never asked
324
- * (every call written before this, and every test config) leaves it undefined,
325
- * and that reads as Claude Code being here, which is what it always meant.
131
+ * What runs on this Mac when nobody has chosen: Codex only when `found.claude`
132
+ * is explicitly false and Codex was found. Undefined means a caller that never
133
+ * asked, and reads as Claude Code being here.
326
134
  */
327
135
  export function homeEngine(found = {}) {
328
136
  return found.claude === false && found.codex ? 'codex' : DEFAULT_ENGINE;
@@ -334,37 +142,13 @@ export function engineChoiceExists(found = {}) {
334
142
  }
335
143
 
336
144
  /**
337
- * THE RULE. Which engine one work item runs on, and it is deliberately narrow.
338
- *
339
- * The gate answers first: without `enabled: ENGINE_CHOICE_ENABLED` this is
340
- * Claude Code, whatever the row says and whatever the workspace says. That is
341
- * what holds the August rows described at the top of this file, and it means
342
- * the honest answer for any caller that has not been taught about the second
343
- * engine — including every caller written before it comes back — is the engine
344
- * she is running.
345
- *
346
- * Then her choice on the task, over the workspace default, which is hers to
347
- * set in Settings, over Claude Code. An engine named on a row whose binary is
348
- * not on this Mac falls back rather than dying: she may have chosen Codex on
349
- * her laptop and be reading the row on a machine without it, and a session
350
- * that never starts tells her nothing about why. That fallback is unchanged
351
- * and the gate is IN ADDITION to it, not instead of it — the binary is a fact
352
- * about this Mac, the gate is a fact about this build, and either one missing
353
- * is enough to answer Claude Code.
354
- *
355
- * AND A CHOICE FROM BEFORE SHE OPENED THE GATE IS NOT A CHOICE SHE IS MAKING.
356
- * When the config names the moment she opted in, a row whose `engine` predates
357
- * it reads as no choice at all and the workspace default answers instead. That
358
- * is `engineChoiceOnRowIsStale` above, and it is the whole of what makes the
359
- * opt-in mean "Codex may be chosen from now on" rather than "every row that
360
- * ever said codex is live". A caller that passes no such moment gets the old
361
- * rule unchanged, which is why every assertion written before this one still
362
- * holds -- but the single caller in the app that can open the gate derives the
363
- * token FROM that same value, so in her app the two cannot come apart.
145
+ * Which engine one work item runs on. A Mac with only Codex runs Codex.
146
+ * Otherwise, without `enabled: ENGINE_CHOICE_ENABLED` it is Claude Code. With
147
+ * it, the row's engine wins over the workspace default, each ignored if written
148
+ * before the gate's moment. An engine whose binary is not on this Mac falls back
149
+ * to Claude Code, since the row may have been chosen on another machine.
364
150
  */
365
151
  export function engineFor(item, { config = {}, found = {}, enabled } = {}) {
366
- // ONE ENGINE ON THE MAC IS NOT A CHOICE, so the gate has nothing to guard.
367
- // Without Claude Code, a row marked `claude` would only die on arrival.
368
152
  if (homeEngine(found) !== DEFAULT_ENGINE) return homeEngine(found);
369
153
  if (enabled !== ENGINE_CHOICE_ENABLED) return DEFAULT_ENGINE;
370
154
  const wanted = isEngine(item?.engine) && !engineChoiceOnRowIsStale(item, config) ? item.engine : null;
@@ -375,43 +159,20 @@ export function engineFor(item, { config = {}, found = {}, enabled } = {}) {
375
159
  }
376
160
 
377
161
  /**
378
- * Whether the engine on this row is one she chose, as opposed to the one it
379
- * would run on anyway. What makes the composer's footer stay open, the same
380
- * rule the level and the model follow (renderer/src/compose-footer.ts).
162
+ * Whether the row names an engine other than the default. The composer's footer
163
+ * stays open on it, like the level and the model (renderer/src/compose-footer.ts).
381
164
  */
382
165
  export function enginePicked(id) {
383
166
  return isEngine(id) && id !== DEFAULT_ENGINE;
384
167
  }
385
168
 
386
169
  /**
387
- * THE MODEL ON THIS ROW, OR NULL BECAUSE IT BELONGS TO THE OTHER HARNESS.
388
- *
389
- * THE MIRROR OF `codexModelRefusal`, WHICH ONLY EVER GUARDED ONE DIRECTION.
390
- * That one stops a Claude alias reaching a Codex run. Nothing stopped the
391
- * reverse, and the reverse happens BY ITSELF: `engineFor` above falls back to
392
- * Claude Code when Codex is not on this Mac -- which is correct and stays --
393
- * and `spawnPlan` then applied `item.model` without asking which engine wrote
394
- * it. Measured 2026-09-05 by reading the resolved plan: a row saying `engine:
395
- * "codex", model: "gpt-5.6-sol"` -- the slug her own config.toml names --
396
- * produced `claude -p <brief> --model gpt-5.6-sol`, which Claude Code has never
397
- * heard of. It dies on arrival and dies again on every retry.
398
- *
399
- * THE TWO FIELDS ARE ONE CHOICE AND THIS IS WHY. The composer's model list is
400
- * scoped to the engine (`engineModelChoices`, renderer/src/models.ts) because
401
- * the two vocabularies do not overlap at all: `opus` is an alias Claude Code
402
- * resolves, `gpt-5.6-sol` is an exact slug Codex takes. So a model word means
403
- * nothing without the engine beside it, and a row whose engine has changed
404
- * under it has no model, rather than one that reads across.
405
- *
406
- * NULL IS NOT A STAND-IN AND THE CALLER MUST NOT TREAT IT AS ONE. This says
407
- * "there is no model on this row for this run"; whether that means running on
408
- * the workspace's own model or refusing the run outright is the caller's
409
- * decision, and main/supervisor.mjs makes it -- it refuses, and says so on the
410
- * row, for the reason `codexModelRefusal` gives on the other side.
411
- *
412
- * A ROW THAT NAMES NO ENGINE IS UNTOUCHED, which is every row she has ever
413
- * written: there is no other harness in the sentence, so the model rides
414
- * exactly as it did.
170
+ * The row's model for a run on `engine`, or null when the row names a different
171
+ * engine. Claude and Codex model names do not overlap, so a Codex slug must not
172
+ * reach `claude --model` when `engineFor` falls back to Claude Code. This is the
173
+ * mirror of `codexModelRefusal`. Null means "no model for this run", and
174
+ * main/supervisor.mjs decides what to do with it. A row naming no engine keeps
175
+ * its model.
415
176
  */
416
177
  export function modelForEngine(item, engine) {
417
178
  const model = typeof item?.model === 'string' && item.model ? item.model : null;
@@ -12,3 +12,10 @@ export function feedbackPayload(p?: {
12
12
  files?: { name: string; type: string; data: string }[];
13
13
  app?: { version?: string; os?: string };
14
14
  }): { text: string; files: { name: string; type: string; data: string }[]; app: { version: string; os: string } };
15
+
16
+ export function sendFeedback(p: {
17
+ url: string | null | undefined;
18
+ payload: { text: string; files: { name: string; type: string; data: string }[]; app?: { version: string; os: string } };
19
+ fetchImpl?: (url: string, init: RequestInit) => Promise<{ ok: boolean }>;
20
+ timeoutMs?: number;
21
+ }): Promise<{ ok: boolean; error?: string }>;
@@ -5,6 +5,7 @@
5
5
  //
6
6
  // Words, files, or both, and never nothing. The size limit keeps one request
7
7
  // well inside what the mail provider takes as attachments.
8
+ import { Name } from './product-name.mjs';
8
9
 
9
10
  export const FEEDBACK_LIMITS = Object.freeze({
10
11
  text: 20_000,
@@ -42,3 +43,27 @@ export function feedbackPayload({ text = '', files = [], app = {} } = {}) {
42
43
  app: { version: String(app.version ?? ''), os: String(app.os ?? '') },
43
44
  };
44
45
  }
46
+
47
+ // Answers { ok: true } or { ok: false, error } with the error as a sentence.
48
+ // It never throws, so the card always has something to say. The Mac sends
49
+ // through it (main/feedback.mjs) and so does the phone website (cloud-bridge.ts).
50
+ export async function sendFeedback({ url, payload, fetchImpl = globalThis.fetch, timeoutMs = 60_000 }) {
51
+ if (!url) return { ok: false, error: `Feedback is not set up in this copy of ${Name}.` };
52
+ // Checked again here, off the bytes that would actually travel, since the
53
+ // card is not the last word on what leaves the device.
54
+ const sized = (payload?.files ?? []).map((f) => ({ size: Math.floor((String(f?.data ?? '').length * 3) / 4) }));
55
+ const check = checkFeedback({ text: payload?.text, files: sized });
56
+ if (!check.ok) return { ok: false, error: check.reason };
57
+ try {
58
+ const res = await fetchImpl(url, {
59
+ method: 'POST',
60
+ headers: { 'content-type': 'application/json' },
61
+ body: JSON.stringify(payload),
62
+ signal: AbortSignal.timeout(timeoutMs),
63
+ });
64
+ if (res?.ok) return { ok: true };
65
+ return { ok: false, error: 'It did not send. Try again in a moment.' };
66
+ } catch {
67
+ return { ok: false, error: 'It did not send. Check your connection and try again.' };
68
+ }
69
+ }
@@ -267,47 +267,40 @@ export const PRACTICE_ROWS = [
267
267
  * off the screen by `walkRows` until the tour. Each `state` is the column it
268
268
  * lands in, read off the row by the app's own rules and not by this file:
269
269
  *
270
- * * `needs`: an agent finished, or stopped on a question. Needs you. Only on
271
- * the board, never in the tour, because the tour opens on "Your inbox is
272
- * now empty" and these would make that untrue.
273
270
  * * `working`: sent and not finished. In progress.
274
271
  * * `scheduled`: set for tomorrow morning. Scheduled.
275
272
  *
276
273
  * None is finished, so "Everything you finished is kept here" stays true of
277
- * the Closed tab. */
274
+ * the Closed tab.
275
+ *
276
+ * AND NOT ONE OF THEM MAY EVER NEED YOU (2026-10-24). Two were `needs`, held
277
+ * back from the tour and let through from the board on, so pressing B put two
278
+ * rows into a Needs you the tour had just called empty. The same two pieces of
279
+ * work are still here in states that ask nothing of anybody, which keeps both
280
+ * rules: something in every column, and the inbox stays at zero. */
278
281
  export const PRACTICE_BACKDROP = [
279
282
  {
280
- state: 'needs',
283
+ state: 'working',
281
284
  kind: 'task',
282
- title: 'Booked the train for the offsite.',
285
+ title: 'Book the train for the offsite.',
283
286
  body: 'Book train tickets for the team to the offsite on Thursday morning.',
284
287
  trace: [
285
288
  '[Read] Offsite plan.docx',
286
289
  '[Read] Team contacts.xlsx',
287
290
  'Twelve people, all leaving from the city on Thursday morning.',
288
- '[Write] Train booking.pdf',
291
+ 'Comparing the 8:10 and the 9:10, which is the last one that still gets everybody there in time.',
289
292
  ],
290
- result: 'Twelve seats on the 8:10 on Thursday, all in one carriage. The tickets are in the offsite folder.',
291
293
  agoMs: 9 * 60_000,
292
294
  },
293
295
  {
294
- state: 'needs',
295
- kind: 'question',
296
- title: 'Should the welcome pack go out Monday or Wednesday?',
297
- body: 'Send the welcome pack to the two new starters, and check the day with me first.',
298
- trace: [
299
- '[Read] Welcome pack.docx',
300
- '[Read] Team calendar.ics',
301
- 'One starter begins on Monday and the other on Wednesday.',
302
- ],
303
- result: [
304
- 'One new starter begins Monday and the other Wednesday. Sending both on Monday means nobody waits.',
305
- '',
306
- '## Options',
307
- '1. Monday, for both (recommended)',
308
- '2. On each person\'s first day',
309
- ].join('\n'),
296
+ state: 'scheduled',
297
+ kind: 'task',
298
+ title: 'Send the welcome pack to the two new starters.',
299
+ body: 'Send the welcome pack to the two new starters on Monday, before either of them begins.',
300
+ trace: [],
310
301
  agoMs: 26 * 60_000,
302
+ // Monday morning, counted from when the practice project is made.
303
+ inMs: 66 * 3_600_000,
311
304
  },
312
305
  {
313
306
  state: 'working',
@@ -13,6 +13,14 @@ export const HARNESS_DEFINITIONS = freeze([
13
13
  installer: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
14
14
  loginArgs: ['auth', 'login', '--claudeai'], statusArgs: ['auth', 'status', '--json'],
15
15
  credentialFiles: ['.claude.json', '.credentials.json'],
16
+ // THE OLDEST CLAUDE CODE THE WORKER LINE BELOW RUNS ON. Measured 2026-10-15
17
+ // by running the app's own line against npm releases on a stand-in model:
18
+ // 2.1.71 (2026-03-06) is the first that takes `--permission-mode auto`,
19
+ // which every worker is started with; 2.1.70 and older die in under a
20
+ // second on it. Every other flag is older still (`--effort` arrived after
21
+ // 2.1.8, `--include-partial-messages` after 1.0.90). shared/spawn-trouble.mjs
22
+ // names it when an older one refuses the line.
23
+ minVersion: '2.1.71',
16
24
  // HOW A WORKER IS STARTED, AS DATA. This was six hardcoded flag names in
17
25
  // main/supervisor.mjs, which made the cheapest possible new engine — one
18
26
  // that speaks this same stream-json and differs by a binary and the