agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,389 @@
1
+ // WHICH CODING AGENT A TASK IS HANDED TO.
2
+ //
3
+ // AND THE PREMISE SHE GAVE FOR IT DID NOT HOLD. Measured head to head on her
4
+ // Mac the same day, identical task, four fresh repos each: Codex 16.8 / 45.3 /
5
+ // 96.9 / 18.6 seconds, Claude Code 19.0 / 19.8 / 19.8 / 21.2. Both correct
6
+ // every run. Codex is NOT faster; it is slower on median and it wanders. That
7
+ // is written here rather than in a report because it is the reason this file
8
+ // must never make Codex the default for anybody, including her. Claude Code is
9
+ // the default and stays the default until she says otherwise. What this file
10
+ // buys is a SECOND ENGINE on a second subscription, which is the thing that
11
+ // actually went wrong for her, and a choice she makes per task.
12
+ //
13
+ // THIS FILE WAS DELETED AND IS COMING BACK IN STAGES, WHICH IS WHY IT NOW HAS
14
+ // A GATE. Her own message on that commit says thirteen; the number here is the
15
+ // one the tree can be asked for.
16
+ //
17
+ // `engine` DID NOT GO. It is still a live name on the work-item contract
18
+ // (`WORK_ITEM_FIELDS`, shared/work-items.mjs:95, accepted by the patch
19
+ // validator at :290, stored as a free string and validated against nothing,
20
+ // because "nothing here reads them"). The fold has no migration and drops
21
+ // nothing, so a row marked between 08-25 and 08-27 still carries
22
+ // `engine: "codex"` UNLESS a later line overwrote that one field — which is
23
+ // possible: the fold lets an equal-or-stronger write replace a field held by
24
+ // an earlier one (`beats`, the authority comparison near shared/work-items.mjs
25
+ // :494). Nothing has ever been written to clear it on purpose, so the honest
26
+ // statement is "probably still there", not "always will be".
27
+ //
28
+ // HOW MANY SUCH ROWS SHE ACTUALLY HAS IS AN OPEN QUESTION, AND IT IS NOT ZERO
29
+ // UNTIL SOMEBODY LOOKS. It could not be measured from the machine this was
30
+ // written on: there is no ~/Zero there at all. On her Mac it is one command,
31
+ // `grep -rho '"engine":"[a-z]*"' ~/Zero | sort | uniq -c`, and the answer
32
+ // belongs in this paragraph when someone runs it. The gate below costs nothing
33
+ // if the count turns out to be zero, and is the whole story if it is not.
34
+ //
35
+ // So the 08-25 resolver, restored unchanged, would have been a silent reversal
36
+ // of her decision: those rows would start choosing Codex again the moment this
37
+ // module landed, with no picker, nothing in Settings, and no approval path
38
+ // built. The resolver is therefore CLOSED unless the caller hands it
39
+ // `ENGINE_CHOICE_ENABLED` below. A caller that passes nothing gets Claude
40
+ // Code, because the safe answer is the one that has to be the accident.
41
+ //
42
+ // This module holds the names and the one rule. Everything Codex-shaped —
43
+ // arguments, its event stream, where it writes its transcript — belongs in
44
+ // main/codex.mjs when that comes back, because the renderer must never import
45
+ // spawn code.
46
+
47
+ /** The engines, in the order the picker draws them. */
48
+ export const ENGINES = [
49
+ { id: 'claude', label: 'Claude Code', word: 'Claude Code' },
50
+ { id: 'codex', label: 'Codex', word: 'Codex' },
51
+ ];
52
+
53
+ export const ENGINE_IDS = ENGINES.map((e) => e.id);
54
+
55
+ /**
56
+ * What a task runs on when nobody has chosen.
57
+ *
58
+ * Not a setting and not a coin flip. See the measurement above: Claude Code is
59
+ * faster on median and never wandered, and it is the only one of the two that
60
+ * is WIRED to stop and ask her for permission mid-run.
61
+ *
62
+ * That second clause used to say Codex CANNOT ask, and it is now wrong twice
63
+ * over. It was true of `codex exec`, which is what the removed 08-25 build
64
+ * drove and what the sentence was written about. `codex app-server` raises real
65
+ * approval requests, measured 2026-09-04, main/codex-app-server.mjs answers
66
+ * them, main/codex-approvals.mjs draws them as the card she already knows, and
67
+ * the spool they land in is the one the other engine uses. This paragraph went
68
+ * on listing all four of those as missing for a whole slice after two of them
69
+ * had been built.
70
+ *
71
+ * THE PICKER IS BUILT NOW TOO, 2026-09-04: a word in the composer's sentence
72
+ * ("With Codex."), a row in Settings, and the engine named in the byline --
73
+ * every one of them drawn only where `Supervisor#engineChoices` says this Mac
74
+ * really offers a choice, which means the gate is open and the binary is there.
75
+ * That paragraph used to say the missing picker was why the default had not
76
+ * moved, and it was the last of four such claims to stop being true.
77
+ *
78
+ * SO THE DEFAULT IS HELD BY THE MEASUREMENT AT THE TOP OF THIS FILE AND BY
79
+ * NOTHING ELSE. Codex is slower on median and it wanders; Claude Code stays the
80
+ * default until she says otherwise, and now that there is a picker that is a
81
+ * decision rather than a consequence.
82
+ */
83
+ export const DEFAULT_ENGINE = 'claude';
84
+
85
+ /**
86
+ * THE CAPABILITY GATE: an explicit code opt-in, saying that whoever wrote this
87
+ * call site knew there was a second engine and meant to allow it.
88
+ *
89
+ * BE PRECISE ABOUT WHAT IT PROVES, because a gate believed to be stronger than
90
+ * it is, is worse than no gate. It is a hard barrier against SERIALIZED DATA:
91
+ * nothing that arrives as a ledger line, a config file or an IPC message can
92
+ * open it, ever. It is NOT proof that the approval path she needs was actually
93
+ * built, and it is not a defence against our own code — any module in this repo
94
+ * can import the token. It moves the decision from data to source, and that is
95
+ * all it does. It is the second half, the picker and her consent, that makes
96
+ * the choice real.
97
+ *
98
+ * It is a Symbol, and that shape is the whole design:
99
+ *
100
+ * - A SYMBOL CANNOT ARRIVE FROM OUTSIDE THE PROCESS. Every input that could
101
+ * carry a stale `codex` is serialized: the ledger line off disk and
102
+ * `zero.config.json` are JSON, which has no way to spell a Symbol and drops
103
+ * it in silence, and an Electron IPC payload goes through the structured
104
+ * clone algorithm, which does not drop a Symbol but THROWS DataCloneError on
105
+ * one. Neither can express the gate. The louder of the two failures is the
106
+ * better one and we get it where it matters least; the point is that the
107
+ * untrusted data and the permission live in different type systems.
108
+ * - IT IS `Symbol`, NEVER `Symbol.for`. The global registry would let any code
109
+ * anywhere mint an equal one out of a string. This one exists only where
110
+ * somebody imported this module and named it.
111
+ * - IT IS NOT A BOOLEAN. `enabled: true` is the kind of thing a caller spreads
112
+ * in by accident from some other options object. This is not.
113
+ */
114
+ export const ENGINE_CHOICE_ENABLED = Symbol('the second engine is wired up in this build');
115
+
116
+ /**
117
+ * WHEN SHE OPENED THE GATE, read off `zero.config.json`, or null. This is the
118
+ * one value in the app that can cause the Symbol above to be handed over
119
+ * (main/supervisor.mjs `_engineFor`, the only holder of it).
120
+ *
121
+ * THE OPT-IN IS A MOMENT AND NOT A FLAG, and that is the whole design of it.
122
+ *
123
+ * `engine` survived the 2026-08-27 removal on the work-item contract and the
124
+ * fold drops nothing, so her store may still hold rows marked `codex` between
125
+ * 08-25 and 08-27. How many is unknown; on her Mac it is one command,
126
+ * `grep -rho '"engine":"[a-z]*"' ~/Zero | sort | uniq -c`, and the answer
127
+ * belongs here when somebody runs it. A boolean opt-in would mean "every row
128
+ * that ever said codex is live from now on", and the first thing it would do is
129
+ * route an unknown number of those onto a second engine she had marked before
130
+ * there was any way to choose. That is the exact hazard the gate was built for,
131
+ * so an opt-in that walks into it is not one worth having. The picker built on
132
+ * 2026-09-04 does not soften this: an August row is still not a choice she is
133
+ * making now, and the byline draws it as the engine that will really run it.
134
+ *
135
+ * THOSE TWO MEANINGS ARE SEPARABLE, AND THE LEDGER ALREADY HOLDS WHAT
136
+ * SEPARATES THEM. The fold records `wrote[field] = { ts, source }` per field
137
+ * and stopped deleting it on the way out precisely because readers needed it
138
+ * (shared/work-items.mjs:324; shared/answers.mjs reads `wrote.answer.ts` the
139
+ * same way). So "Codex may be chosen FROM NOW ON" is a checkable statement
140
+ * rather than a hope, and `engineChoiceOnRowIsStale` below is the check.
141
+ *
142
+ * WHY NOT A BOOLEAN PLUS A REMEMBERED "first seen at". Because that would live
143
+ * in the app's own state rather than in her file: a fresh install, a state file
144
+ * she deleted, or a second Mac would each invent a different moment, and the
145
+ * one thing this value must be is the same on every machine reading the same
146
+ * store. A date she typed is a fact about her decision; a date we noticed is a
147
+ * fact about a process.
148
+ *
149
+ * THE WORKSPACE DEFAULT (`config.engine`) IS TIME-SCOPED TOO, SINCE 2026-09-05,
150
+ * AND THIS PARAGRAPH USED TO SAY IT COULD NOT BE. Its words were: "JSON has no
151
+ * per-key write times, and `engine` sits in the same file as this, so opening
152
+ * that file to write the moment is the same act as reading the line above it."
153
+ * The first clause is true of JSON and irrelevant: the APP has write times, one
154
+ * module writes that key (`setWorkspaceSetting`, main/settings.mjs), and
155
+ * `saveConfig` takes a patch of several keys, so `engineAt` rides in the same
156
+ * write. Without it, closing the gate and reopening it with a NEWER moment
157
+ * re-armed a workspace default set weeks earlier -- every row that names
158
+ * nothing went straight back to Codex without her saying so again, which is the
159
+ * exact claim this value is supposed to make impossible, and the rollback
160
+ * lifecycle is the thing the gate is FOR.
161
+ *
162
+ * AND THE SECOND CLAUSE, READ CORRECTLY, IS WHY AN ABSENT STAMP MEANS HONOURED.
163
+ * `engineChoice` is hand-written into this file; a hand-written `engine` on the
164
+ * line above it was put there by the same hand in the same act, so the two ARE
165
+ * contemporaneous and there is nothing to be stale against. Only a default the
166
+ * app wrote can carry a stamp, and only a stamped default can go stale — which
167
+ * is also what keeps every config that exists today behaving exactly as it did.
168
+ * `engineDefaultIsStale` below is the check.
169
+ *
170
+ * IT IS PARSED STRICTLY, WHICH IS NOT THE SAME AS PARSED. `Date.parse` reads
171
+ * "1", "2026", "Sep 4 2026" and "2026/09/04" as real dates, so a bare parse
172
+ * would let `"engineChoice": "1"` open the second engine. The shape is checked
173
+ * first and everything else is off, because the safe answer is the one that has
174
+ * to be the accident.
175
+ */
176
+ const A_MOMENT = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
177
+
178
+ /**
179
+ * One strict parse, used by both moments in this file, so the two cannot come
180
+ * to disagree about what counts as a date somebody wrote down. */
181
+ function momentIn(written) {
182
+ if (typeof written !== 'string' || !A_MOMENT.test(written)) return null;
183
+ const at = Date.parse(written);
184
+ return Number.isFinite(at) ? at : null;
185
+ }
186
+
187
+ export function engineChoiceSince(config = {}) {
188
+ return momentIn(config?.engineChoice);
189
+ }
190
+
191
+ /**
192
+ * WHEN THE WORKSPACE DEFAULT WAS SET, read off `zero.config.json`, or null.
193
+ *
194
+ * Null means no stamp, and no stamp means a hand-written default — see the long
195
+ * note above `engineChoiceSince`. It is the same strict parse for the same
196
+ * reason: a claim that cannot be read is not a claim, so it reads as no stamp,
197
+ * which is the hand-written case.
198
+ */
199
+ export function engineDefaultSince(config = {}) {
200
+ return momentIn(config?.engineAt);
201
+ }
202
+
203
+ /**
204
+ * Whether the workspace default is one she set BEFORE the gate's current
205
+ * moment: a setting from the build before the rollback, rather than a decision
206
+ * she is making now.
207
+ *
208
+ * False when nobody has opted in, because there is no moment to be stale
209
+ * against. False when the default is Claude Code, which asks for nothing the
210
+ * gate withholds — the same rule `engineChoiceOnRowIsStale` follows below, and
211
+ * for the same reason: discarding it could only ever move work TO the second
212
+ * engine, which is the one direction the gate exists to prevent. False when
213
+ * there is no stamp, which is a default she typed beside the moment itself.
214
+ */
215
+ export function engineDefaultIsStale(config = {}) {
216
+ const since = engineChoiceSince(config);
217
+ if (since === null) return false;
218
+ if (!enginePicked(config?.engine)) return false;
219
+ const at = engineDefaultSince(config);
220
+ return at !== null && at < since;
221
+ }
222
+
223
+ /**
224
+ * Whether the engine named on this row is one picked BEFORE she opened the
225
+ * gate: an August row, rather than a decision she is making now.
226
+ *
227
+ * False when nobody has opted in, because there is no moment to be stale
228
+ * against and the gate itself is what refuses then. False when the row names no
229
+ * engine, or names one nothing recognises, because neither is asking for
230
+ * anything and a warning on an ordinary row is noise on every row. True when
231
+ * the row names an engine and there is no `wrote.engine.ts` at or after the
232
+ * moment — which also covers an imported session and an old line with no
233
+ * `wrote` map at all, where there is simply no evidence she chose anything.
234
+ *
235
+ * AND FALSE FOR A ROW THAT NAMES CLAUDE CODE, WHICH THIS ASKED WITH `isEngine`
236
+ * AND GOT BACKWARDS. A row saying `engine: "claude"`, written before the moment,
237
+ * was discarded as stale — and `engineFor` then fell through to `config.engine`,
238
+ * which may be `codex`. So her explicit "run this on Claude Code" was read as no
239
+ * choice at all and answered with the second engine, which is the precise
240
+ * opposite of what staleness is for. `enginePicked` is the fix and it is the
241
+ * whole of it: naming the DEFAULT asks for nothing the gate withholds, so there
242
+ * is nothing to be stale about, and discarding it could only ever move work onto
243
+ * the engine the gate was built to hold back.
244
+ *
245
+ * `wrote.engine.source` is deliberately NOT consulted. would be the stronger
246
+ * rule, but CLAUDE.md is explicit that a ledger line names its own author as a
247
+ * plain string and nothing checks it, so a source check would guard against an
248
+ * accident and never against a hostile worker, while silently refusing whatever
249
+ * a future picker happens to write. Who may write `engine` at all belongs to
250
+ * the slice that draws one.
251
+ */
252
+ export function engineChoiceOnRowIsStale(item, config = {}) {
253
+ const since = engineChoiceSince(config);
254
+ if (since === null) return false;
255
+ if (!enginePicked(item?.engine)) return false;
256
+ const ts = item?.wrote?.engine?.ts;
257
+ return !(Number.isFinite(ts) && ts >= since);
258
+ }
259
+
260
+ export function isEngine(id) {
261
+ return typeof id === 'string' && ENGINE_IDS.includes(id);
262
+ }
263
+
264
+ export function engineLabel(id) {
265
+ return ENGINES.find((e) => e.id === id)?.label ?? ENGINES[0].label;
266
+ }
267
+
268
+ /**
269
+ * Which engines this Mac can actually run, given what the finders found.
270
+ *
271
+ * `found` is a map of id to boolean. An engine whose binary is not on the
272
+ * machine is not offered, because a picker that offers something that cannot
273
+ * run is a picker that hands her a session that dies on arrival. Claude Code
274
+ * is always in the list even when it was not found: the app already refuses to
275
+ * open an inbox without it and says so in its own words (main/claude-bin.mjs),
276
+ * and a second sentence here saying the same thing differently is noise.
277
+ *
278
+ * UNGATED, AND THE INVARIANT IS THEREFORE SCOPED TO ROUTING. Say it plainly,
279
+ * because the file must not claim more than the code delivers: this function,
280
+ * `engineChoiceExists` and `enginePicked` will all happily describe a two-engine
281
+ * Mac without anyone holding the token. A future renderer could draw a picker
282
+ * out of them, and the gate would not stop it — she would be offered a choice
283
+ * that `engineFor` then refuses, which is its own kind of lie.
284
+ *
285
+ * That is a deliberate trade and not an oversight. These three answer "what is
286
+ * on this Mac" and "what did she pick"; they spawn nothing, and requiring a
287
+ * capability token to answer a question about the filesystem buys nothing real.
288
+ *
289
+ * AND THE SLICE THAT DREW A PICKER ANSWERED FOR IT, 2026-09-04. There is
290
+ * exactly one caller, `Supervisor#engineChoices`, and it asks
291
+ * `engineChoiceSince` FIRST -- the same expression `_engineFor` derives the gate
292
+ * token from -- so the offer and the routing are refused by the same fact and
293
+ * cannot come apart. A Mac with Codex installed and no moment written answers
294
+ * one engine here and Claude Code there. The renderer calls none of these: it
295
+ * is handed the list on the snapshot. tests/a-codex-row-from-august-still-runs-on-claude.test.mjs
296
+ * holds the caller list exactly, so the next one has to argue for itself here.
297
+ */
298
+ export function availableEngines(found = {}) {
299
+ return ENGINES.filter((e) => e.id === DEFAULT_ENGINE || found[e.id]);
300
+ }
301
+
302
+ /** Whether there is a choice to make at all. One engine is not a choice. */
303
+ export function engineChoiceExists(found = {}) {
304
+ return availableEngines(found).length > 1;
305
+ }
306
+
307
+ /**
308
+ * THE RULE. Which engine one work item runs on, and it is deliberately narrow.
309
+ *
310
+ * The gate answers first: without `enabled: ENGINE_CHOICE_ENABLED` this is
311
+ * Claude Code, whatever the row says and whatever the workspace says. That is
312
+ * what holds the August rows described at the top of this file, and it means
313
+ * the honest answer for any caller that has not been taught about the second
314
+ * engine — including every caller written before it comes back — is the engine
315
+ * she is running.
316
+ *
317
+ * Then her choice on the task, over the workspace default, which is hers to
318
+ * set in Settings, over Claude Code. An engine named on a row whose binary is
319
+ * not on this Mac falls back rather than dying: she may have chosen Codex on
320
+ * her laptop and be reading the row on a machine without it, and a session
321
+ * that never starts tells her nothing about why. That fallback is unchanged
322
+ * and the gate is IN ADDITION to it, not instead of it — the binary is a fact
323
+ * about this Mac, the gate is a fact about this build, and either one missing
324
+ * is enough to answer Claude Code.
325
+ *
326
+ * AND A CHOICE FROM BEFORE SHE OPENED THE GATE IS NOT A CHOICE SHE IS MAKING.
327
+ * When the config names the moment she opted in, a row whose `engine` predates
328
+ * it reads as no choice at all and the workspace default answers instead. That
329
+ * is `engineChoiceOnRowIsStale` above, and it is the whole of what makes the
330
+ * opt-in mean "Codex may be chosen from now on" rather than "every row that
331
+ * ever said codex is live". A caller that passes no such moment gets the old
332
+ * rule unchanged, which is why every assertion written before this one still
333
+ * holds -- but the single caller in the app that can open the gate derives the
334
+ * token FROM that same value, so in her app the two cannot come apart.
335
+ */
336
+ export function engineFor(item, { config = {}, found = {}, enabled } = {}) {
337
+ if (enabled !== ENGINE_CHOICE_ENABLED) return DEFAULT_ENGINE;
338
+ const wanted = isEngine(item?.engine) && !engineChoiceOnRowIsStale(item, config) ? item.engine : null;
339
+ const fallback = isEngine(config?.engine) && !engineDefaultIsStale(config) ? config.engine : DEFAULT_ENGINE;
340
+ const pick = wanted ?? fallback;
341
+ if (pick === DEFAULT_ENGINE) return DEFAULT_ENGINE;
342
+ return found[pick] ? pick : DEFAULT_ENGINE;
343
+ }
344
+
345
+ /**
346
+ * Whether the engine on this row is one she chose, as opposed to the one it
347
+ * would run on anyway. What makes the composer's footer stay open, the same
348
+ * rule the level and the model follow (renderer/src/compose-footer.ts).
349
+ */
350
+ export function enginePicked(id) {
351
+ return isEngine(id) && id !== DEFAULT_ENGINE;
352
+ }
353
+
354
+ /**
355
+ * THE MODEL ON THIS ROW, OR NULL BECAUSE IT BELONGS TO THE OTHER HARNESS.
356
+ *
357
+ * THE MIRROR OF `codexModelRefusal`, WHICH ONLY EVER GUARDED ONE DIRECTION.
358
+ * That one stops a Claude alias reaching a Codex run. Nothing stopped the
359
+ * reverse, and the reverse happens BY ITSELF: `engineFor` above falls back to
360
+ * Claude Code when Codex is not on this Mac -- which is correct and stays --
361
+ * and `spawnPlan` then applied `item.model` without asking which engine wrote
362
+ * it. Measured 2026-09-05 by reading the resolved plan: a row saying `engine:
363
+ * "codex", model: "gpt-5.6-sol"` -- the slug her own config.toml names --
364
+ * produced `claude -p <brief> --model gpt-5.6-sol`, which Claude Code has never
365
+ * heard of. It dies on arrival and dies again on every retry.
366
+ *
367
+ * THE TWO FIELDS ARE ONE CHOICE AND THIS IS WHY. The composer's model list is
368
+ * scoped to the engine (`engineModelChoices`, renderer/src/models.ts) because
369
+ * the two vocabularies do not overlap at all: `opus` is an alias Claude Code
370
+ * resolves, `gpt-5.6-sol` is an exact slug Codex takes. So a model word means
371
+ * nothing without the engine beside it, and a row whose engine has changed
372
+ * under it has no model, rather than one that reads across.
373
+ *
374
+ * NULL IS NOT A STAND-IN AND THE CALLER MUST NOT TREAT IT AS ONE. This says
375
+ * "there is no model on this row for this run"; whether that means running on
376
+ * the workspace's own model or refusing the run outright is the caller's
377
+ * decision, and main/supervisor.mjs makes it -- it refuses, and says so on the
378
+ * row, for the reason `codexModelRefusal` gives on the other side.
379
+ *
380
+ * A ROW THAT NAMES NO ENGINE IS UNTOUCHED, which is every row she has ever
381
+ * written: there is no other harness in the sentence, so the model rides
382
+ * exactly as it did.
383
+ */
384
+ export function modelForEngine(item, engine) {
385
+ const model = typeof item?.model === 'string' && item.model ? item.model : null;
386
+ if (!model) return null;
387
+ if (isEngine(item?.engine) && item.engine !== engine) return null;
388
+ return model;
389
+ }
@@ -0,0 +1,60 @@
1
+ // BEAT EIGHT: THE THREE EXAMPLE ROWS, AND CLEARING THEM IS THE POINT.
2
+ //
3
+ // STAGED IS THE ONE SHE TOOK, off the round four page: "Build it, with the
4
+ // staged tasks on beat eight (recommended)", answered "perfect! build it". The
5
+ // alternative was really running three of the agents just imported, which
6
+ // spends somebody's tokens in their first minute and does nothing at all on a
7
+ // Mac that had no agent files on it.
8
+ //
9
+ // THE THREE SHAPES ARE FROM ROUND ONE: a question, a piece of finished work,
10
+ // and a decision an agent made without her. Between them they are the whole of
11
+ // what an inbox holds and the three different things clearing one can mean.
12
+ //
13
+ // EVERY ONE OF THEM SAYS "Example" FIRST, IN THE TITLE AS WELL AS THE RESULT.
14
+ // Somebody who clears three tasks in an onboarding and later finds them in
15
+ // their closed list should never have to wonder whether they were real.
16
+ //
17
+ // This file has no window and no filesystem in it so both halves can read it:
18
+ // the renderer draws the walk off ../renderer/src/onboarding.ts, and the main
19
+ // process writes the rows off here.
20
+
21
+ export const EXAMPLES = [
22
+ {
23
+ kind: 'question',
24
+ title: 'Example: which of these two names should the project use?',
25
+ result: 'Example. Both are free. Say either one and I will rename everything to match.',
26
+ agoMs: 4 * 60_000,
27
+ },
28
+ {
29
+ kind: 'task',
30
+ title: 'Example: the sign in page is built and the tests are green.',
31
+ result: 'Example. Nothing here needs you. Close it and it is gone.',
32
+ agoMs: 11 * 60_000,
33
+ },
34
+ {
35
+ kind: 'review',
36
+ title: 'Example: I took the smaller library, say if you disagree.',
37
+ result: 'Example. This is what an agent looks like when it made a call without you.',
38
+ agoMs: 26 * 60_000,
39
+ },
40
+ ];
41
+
42
+ /**
43
+ * WHAT PUTS A ROW IN HER INBOX, and it is not the kind or the label: it is
44
+ * `answeredHerAsk` in renderer/src/list-rules.ts, which is her own open row
45
+ * carrying a result an agent wrote AFTER her last word on it. So each example
46
+ * is written twice, her half and then the agent's half, and the agent's half
47
+ * has to carry the later timestamp or the row is real and simply never drawn.
48
+ *
49
+ * This returns the two writes for one example, in order, with the timestamps
50
+ * already worked out. Pure, so the ordering is tested rather than hoped for:
51
+ * tests/the-walk-stages-three-examples.test.mjs. */
52
+ export function stampsFor(example, now) {
53
+ const at = now - example.agoMs;
54
+ return {
55
+ // the row exists and it is her ask, at the moment it landed.
56
+ hers: at,
57
+ // The agent's: strictly later, which is what the inbox rule asks for.
58
+ agent: at + 1_000,
59
+ };
60
+ }
@@ -0,0 +1,149 @@
1
+ // THE ONE LINE THE FIRST TASK ANSWERS WITH.
2
+ //
3
+ // So the walk's example task still has a real row, a real reading pane and a
4
+ // real answer, and the answer is read off the folder she just pointed at
5
+ // instead of being reasoned about by a session. The reason is measured and it
6
+ // is on the row: her own first task sat queued for thirteen seconds behind the
7
+ // directive the walk had just created, ran for another forty-seven, and came
8
+ // back in 59.6 seconds while the sentence on screen promised a few. A first
9
+ // task that depends on a live agent inherits the queue, the model, the network
10
+ // and the size of the folder, and a first run cannot promise anything about any
11
+ // of those.
12
+ //
13
+ // WHAT IT ANSWERS WITH still has to be TRUE, because it is also what goes in
14
+ // the sidebar under the project's name.So this never invents a sentence about a
15
+ // project. It reads one that is already written down, and when nothing is
16
+ // written down it says so.
17
+ //
18
+ // Pure on purpose: the reading of the disk is in main/first-run.mjs and the
19
+ // judgement about what a line is worth is here, where a test can hold it.
20
+
21
+ /** As long as a sentence in a list row can be before it is a paragraph. */
22
+ export const MAX_LINE = 180;
23
+
24
+ /**
25
+ * Under this a full stop is an abbreviation rather than the end of a thought.
26
+ * "e.g." and "v1.2" both live below it and no description does. */
27
+ const MIN_SENTENCE = 24;
28
+
29
+ /**
30
+ * A markdown badge line: `[![build](a)](b)`, shields, and nothing else on it.
31
+ * Readmes open with rows of these and none of them says what the thing is. */
32
+ function isBadges(line) {
33
+ const stripped = line.replace(/\[?!\[[^\]]*\]\([^)]*\)\]?\([^)]*\)?/g, '').trim();
34
+ return stripped === '' && /!\[/.test(line);
35
+ }
36
+
37
+ /**
38
+ * A line that is furniture rather than prose.
39
+ *
40
+ * THE LAST FOUR ARE MEASURED, not imagined. Run over the 29 project folders in
41
+ * ~/Desktop/dev on 2026-08-21, the first cut of this returned "Run the
42
+ * application with the following command:" for one project, "To build the
43
+ * project, run:" for another, a shields.io link definition for a third and
44
+ * "Built with: Next.js 16 | FastAPI | DeepAgents | ..." for a fourth. Every one
45
+ * of those is a true line out of a readme and none of them says what the thing
46
+ * IS, which is the question the task asks. */
47
+ function isFurniture(line) {
48
+ if (!line) return true;
49
+ if (line.startsWith('<!--') || line.startsWith('<')) return true; // html comment, or a centred div
50
+ if (/^[-=*_]{3,}$/.test(line)) return true; // a rule
51
+ if (/^[-*+]\s/.test(line) || /^\d+[.)]\s/.test(line)) return true; // a list item is not a description
52
+ if (line.startsWith('```') || line.startsWith(' ')) return true; // code
53
+ if (line.startsWith('|') || line.startsWith('>')) return true; // a table row, a blockquote
54
+ if (isBadges(line)) return true;
55
+ if (/^\[[^\]]+\]:\s/.test(line)) return true; // a link definition at the foot of a readme
56
+ if (line.endsWith(':')) return true; // a lead-in to a command, not a description
57
+ if ((line.match(/\|/g) ?? []).length >= 2) return true; // a stack list, not a sentence
58
+ if (/^(install|usage|getting started|quick ?start|setup|license|contributing)\b/i.test(line)) return true;
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Markdown out, words left. Links keep their text, emphasis and code lose
64
+ * their marks, and a trailing full stop is kept because it is a sentence. */
65
+ export function plainText(line) {
66
+ return line
67
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
68
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
69
+ .replace(/`([^`]*)`/g, '$1')
70
+ .replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1')
71
+ .replace(/\s+/g, ' ')
72
+ .trim();
73
+ }
74
+
75
+ /**
76
+ * THE FIRST REAL LINE OF A README. The heading at the top is the project's
77
+ * name, which she already typed on the screen before this one, so it is not
78
+ * the answer: what is wanted is the first line of prose under it. A readme
79
+ * with nothing but a heading in it returns null rather than the heading. */
80
+ export function fromReadme(text) {
81
+ if (!text) return null;
82
+ let inFence = false;
83
+ for (const raw of String(text).split(/\r?\n/)) {
84
+ const line = raw.trim();
85
+ if (line.startsWith('```')) { inFence = !inFence; continue; }
86
+ if (inFence) continue;
87
+ if (line.startsWith('#')) continue; // the name, and every heading after it
88
+ if (isFurniture(line)) continue;
89
+ const words = plainText(line);
90
+ // Two words is the floor. A one word line is a label, not a description.
91
+ if (words.split(' ').length < 2) continue;
92
+ return clip(words);
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * package.json says what a thing is in one field, and people fill it in. Same
99
+ * for Cargo.toml and pyproject.toml, which is why the shape is shared. */
100
+ export function fromDescription(value) {
101
+ if (typeof value !== 'string') return null;
102
+ const words = plainText(value);
103
+ return words.split(' ').filter(Boolean).length >= 2 ? clip(words) : null;
104
+ }
105
+
106
+ /**
107
+ * ONE SENTENCE, AND NOT A PARAGRAPH. The FIRST sentence end rather than the
108
+ * last one that fits, which is the opposite of how the inbox row clips: a row
109
+ * wants as much of the message as the width allows, and this wants the
110
+ * description and not the sentence after it.
111
+ *
112
+ * Only a first sentence long enough to say something counts. Under that, and
113
+ * when there is no sentence end at all, the cut is at a word with an ellipsis,
114
+ * because a line that stops mid-word reads as a bug rather than as a summary. */
115
+ export function clip(text, max = MAX_LINE) {
116
+ const first = text.search(/[.!?](\s|$)/);
117
+ if (first >= MIN_SENTENCE && first < max) return text.slice(0, first + 1);
118
+ if (text.length <= max) return text;
119
+ const cut = text.slice(0, max);
120
+ const space = cut.lastIndexOf(' ');
121
+ return (space > MIN_SENTENCE ? cut.slice(0, space) : cut).replace(/[,;:\s]+$/, '') + '…';
122
+ }
123
+
124
+ /**
125
+ * WHAT THE TASK ANSWERS, given whatever the folder had in it.
126
+ *
127
+ * `readme` and `description` are what was found on disk, either of them null.
128
+ * `name` is what she called the project on the screen before, and `folder` is
129
+ * the short path she chose. The last case is the honest one: a folder with no
130
+ * readme and no description in it has nothing written down that says what it
131
+ * is, and saying so is better than guessing, because this same line goes in
132
+ * the sidebar and stays there. */
133
+ export function firstRunAnswer({ readme, description, name, folder }) {
134
+ const found = fromReadme(readme) ?? fromDescription(description);
135
+ if (found) return { line: found, found: true };
136
+ const where = folder ? ` in ${folder}` : '';
137
+ const who = name || 'This project';
138
+ // TWO DIFFERENT TRUE SENTENCES, because they are two different facts and the
139
+ // first cut said the wrong one. Measured over ~/Desktop/dev on 2026-08-21:
140
+ // agentbox-website HAS a readme, and every line in it is a build instruction,
141
+ // so "has no readme" was simply false about her own folder.
142
+ if (readme && String(readme).trim()) {
143
+ return { line: `${who} has a readme${where}, and no line in it says what the project is.`, found: false };
144
+ }
145
+ return {
146
+ line: `${who} has no readme${where}, so there is nothing written down yet that says what it is.`,
147
+ found: false,
148
+ };
149
+ }
@@ -0,0 +1,32 @@
1
+ // Types for shared/first-run-practice.mjs, which the renderer imports for the
2
+ // practice project's copy. Same pattern as agents.d.mts beside it: the module
3
+ // is plain JavaScript so both halves can read it, and this says what it is.
4
+
5
+ export const PRACTICE_NAME: string;
6
+ export const PRACTICE_SLUG: string;
7
+ export const PRACTICE_FLAG: string;
8
+ export const PRACTICE_TASK: { title: string; body: string };
9
+ export const PRACTICE_ANSWER: string;
10
+ export const PRACTICE_TASK_TRACE: string[];
11
+ /** The practice project's sidebar note, written to its `pinned.md` and drawn
12
+ * beside the third introduction slab. */
13
+ export const PRACTICE_NOTE: string;
14
+ export const PRACTICE_ROWS: {
15
+ kind: string;
16
+ title: string;
17
+ /** The sentence a person typed to start it. */
18
+ body: string;
19
+ /** The lines the run wrote while it worked, in the shape traceStreamLine
20
+ * writes a real one: `[Tool] argument`, or a plain sentence when the agent
21
+ * is talking. The store stamps the times. */
22
+ trace: string[];
23
+ result: string;
24
+ agoMs: number;
25
+ /** THE ONE THAT IS NOT FINISHED. An agent is stopped on it, waiting for an
26
+ * answer, and beat thirteen is about the difference between this row and the
27
+ * other two. Exactly one of the three carries it. */
28
+ waiting?: boolean;
29
+ /** THE ONE THAT IS NOT FOR TODAY. Real work, nobody's emergency, and the row
30
+ * beat fourteen snoozes. Exactly one of the four carries it. */
31
+ later?: boolean;
32
+ }[];