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,599 @@
1
+ // WHAT A DEAD SPAWN IS ALLOWED TO SAY ON HER SCREEN.
2
+ //
3
+ // The old rule was: take the last words of the session that died and print
4
+ // them. That is how she came to be reading "Agents are failing to start: done:
5
+ // Failed to authenticate: OAuth session expired and could not be refreshed"
6
+ // across the top of her inbox. Her three complaints were all correct. There is
7
+ // no OAuth in Agentbox, so the sentence names a thing that does not exist here.
8
+ // It is ugly. And it was on screen while agents were working perfectly well,
9
+ // because it was about one of her two Claude accounts and not about the app.
10
+ //
11
+ // So the raw text never reaches the renderer again. It goes in the session's
12
+ // own trace log, which is where a person debugging wants it, and what she is
13
+ // shown is one sentence of ours written in her words.
14
+ //
15
+ // Every sentence here has to survive the same test: read it out loud to
16
+ // somebody who has never heard of a subprocess, and they know what happened
17
+ // and what to do about it.
18
+
19
+ // The shapes a CLI uses when the account it was handed cannot be used at all.
20
+ // Matching is deliberately loose: these strings have changed wording several
21
+ // times and a miss only costs us the generic sentence, never a crash.
22
+ //
23
+ // THIS SET SAID "the shapes the CLAUDE CLI uses" AND HAD NEVER BEEN POINTED AT
24
+ // CODEX. It is right about Codex anyway, and that is measured rather than
25
+ // assumed. On 2026-09-05 `codex app-server` (codex-cli 0.148.0) was driven the
26
+ // way main/codex-app-server.mjs drives it, with CODEX_HOME pointed at an empty
27
+ // directory so there was no auth.json and her real ~/.codex login was neither
28
+ // read nor written. The turn retried ten times and failed with
29
+ //
30
+ // unexpected status 401 Unauthorized: Missing bearer or basic authentication in
31
+ // header, url: https://api.openai.com/v1/responses, cf-ray:
32
+ //
33
+ // which `/unauthoriz/i` below already catches. NOTHING WAS ADDED HERE, and the
34
+ // absence is the finding: `unauthorized` is also the schema's own code for it
35
+ // (see AT_LIMIT), so the token and the prose land on the same pattern.
36
+ //
37
+ // WORTH KNOWING FOR THE NEXT READER: what came back was the RAW HTTP text, not
38
+ // Codex's own "run codex login" sentence. The app-server passes the transport's
39
+ // words through. tests/a-codex-run-that-is-signed-out-or-capped-says-which
40
+ // holds all of it.
41
+
42
+ // AN ACCOUNT THAT IS SIGNED IN AND NOT ALLOWED TO RUN.
43
+ //
44
+ // She was. Her second login was intact and her browser sign-in succeeded every
45
+ // time she did it; the WORKSPACE that account belongs to has Claude Code turned
46
+ // off. Run by hand on her Mac, this is the whole of what the CLI says:
47
+ //
48
+ // Your organization has disabled Claude subscription access for Claude Code ·
49
+ // Use an Anthropic API key instead, or ask your admin to enable access
50
+ //
51
+ // "subscription access" is in the list below, so every one of those was filed
52
+ // as a login that had run out and she was told to type /login. She did, twice
53
+ // in one afternoon; both logins worked; nothing changed, because no login can
54
+ // change an admin's policy. FOUR dead runs on one row that day, all four
55
+ // carrying "Sign that account back in and it picks straight up."
56
+ //
57
+ // TESTED BEFORE SIGNED_OUT, which is the whole of the fix: these lines say
58
+ // "subscription" and "access" and mean something else entirely.
59
+ //
60
+ // The API-key half of the CLI's sentence is never repeated to her. Workers bill
61
+ // to her subscription and never to a key (CLAUDE.md), so passing that advice on
62
+ // would be this app telling her to do the one thing it refuses to do.
63
+ import { NAME, Name } from '../shared/product-name.mjs';
64
+ const ORG_BLOCKED = [
65
+ /organi[sz]ation has disabled/i,
66
+ /disabled for (your|this) organi[sz]ation/i,
67
+ /ask your admin/i,
68
+ ];
69
+
70
+ const SIGNED_OUT = [
71
+ /oauth/i,
72
+ /session expired/i,
73
+ /could not be refreshed/i,
74
+ /failed to authenticate/i,
75
+ /not logged in/i,
76
+ /please run\s*\/?login/i,
77
+ /invalid api key/i,
78
+ /unauthoriz/i,
79
+ /subscription access/i,
80
+ ];
81
+
82
+ const AT_LIMIT = [
83
+ /usage limit/i,
84
+ /rate limit/i,
85
+ /quota/i,
86
+ /out of credit/i,
87
+ /insufficient credit/i,
88
+ // THE WORDS THE CLI ACTUALLY PRINTS, AND THE REASON SHE WAS TOLD
89
+ // NOTHING.Claude Code never says "usage limit". Not one pattern above
90
+ // matched either, so every limit she ever hit was filed 'unknown' and drew
91
+ // the generic sentence, which for a limit is actively false: it tells her
92
+ // this will keep failing until it is fixed, when it clears itself at the
93
+ // time printed on the same line. MEASURED across all 3,769 trace logs in
94
+ // her store: 50 runs on 27 rows.
95
+ /hit your [a-z]{0,12} ?limit/i,
96
+ /session limit/i,
97
+ /weekly limit/i,
98
+ // AND THE ONE PATTERN IN THIS FILE THAT IS NOT ABOUT ENGLISH.
99
+ //
100
+ // A limit cannot be provoked on purpose without spending her real
101
+ // subscription, so this set could not be measured against Codex the way
102
+ // SIGNED_OUT above was. Two things were done instead of guessing.
103
+ //
104
+ // Every one is already matched by a pattern above, so NO WORDING PATTERN WAS
105
+ // ADDED.
106
+ //
107
+ // SECOND, THE STRUCTURAL ONE. `codex app-server generate-json-schema` -- the
108
+ // binary's own generator, run on this Mac -- defines TurnError as
109
+ // `{ message, additionalDetails, codexErrorInfo }`, and CodexErrorInfo as an
110
+ // enum including `usageLimitExceeded`. That is a name published by the vendor
111
+ // rather than prose that drifts, and main/codex-session.mjs was discarding it
112
+ // and leaving the classifier to read the sentence. It now puts the code in
113
+ // front of the line, where the 300-character window `noteExitForBackoff`
114
+ // takes cannot cut it off, and this is what reads it.
115
+ //
116
+ // NARROW ON PURPOSE. The same enum carries `contextWindowExceeded` and
117
+ // `sessionBudgetExceeded`, and NEITHER is her subscription -- one is the
118
+ // model's context window, one is a per-run budget somebody set. A rule about
119
+ // the word "exceeded" would file both as a limit and tell her to wait for a
120
+ // reset that is never coming.
121
+ /\busageLimitExceeded\b/,
122
+ ];
123
+
124
+ // A RUN THE MACHINE OR THE NETWORK CUT OFF. Nothing is wrong with the account
125
+ // and there is nothing for her to do, which is why this cannot stay inside
126
+ // 'unknown': the generic sentence ends "it will keep failing the same way
127
+ // until this is fixed", and a laptop that went to sleep is not a thing to fix.
128
+ // MEASURED on her store 2026-08-28: 182 of the 239 unnamed runs are these, and
129
+ // the single biggest cause of a dead run anywhere in the app is her own Mac
130
+ // going to sleep mid-response, 133 runs across 63 rows.
131
+ const INTERRUPTED = [
132
+ /went to sleep/i,
133
+ /connection (closed|lost|dropped)/i,
134
+ /response stopped arriving/i,
135
+ /may be incomplete/i,
136
+ /econnreset|enotfound|etimedout|econnrefused/i,
137
+ /unable to connect/i,
138
+ /can.{0,3}t reach the api/i,
139
+ /timed out/i,
140
+ /\b(429|500|502|503|529)\b/,
141
+ /overloaded/i,
142
+ ];
143
+
144
+ // A workspace the CLI refuses to run in. Distinct from the two above because
145
+ // the fix is a folder, not an account.
146
+ const WORKSPACE = [
147
+ /untrusted/i,
148
+ /trust this folder/i,
149
+ /not a git repos/i,
150
+ // CODEX'S OWN WORDING, AND IT MATCHES NONE OF THE THREE ABOVE. What it
151
+ // actually prints, measured 2026-08-27 on codex-cli 0.144.0: "Not inside a
152
+ // trusted directory and --skip-git-repo-check was not specified." There is
153
+ // no "untrusted" in that sentence and no "git repository" either, so every
154
+ // dead Codex spawn was filed 'unknown' and drew the generic sentence --
155
+ // which for this cause is actively wrong, because it tells her the run will
156
+ // keep failing until something is fixed without naming the folder that is
157
+ // the fix.
158
+ //
159
+ // These two lines were here from 2026-08-25 and went out with the whole
160
+ // engine on 08-27 (258d71d). They are back BEFORE anything can route to
161
+ // Codex, deliberately: a cause that is only classified once the first Codex
162
+ // run dies is a cause that is not classified when the first Codex run dies.
163
+ /not inside a trusted directory/i,
164
+ /skip-git-repo-check/i,
165
+ ];
166
+
167
+ // WHICH OF THE THREE THINGS WENT WRONG, from whatever the tool printed.
168
+ // 'unknown' is a first-class answer, not a failure: an honest generic sentence
169
+ // beats a confident wrong one.
170
+ export function troubleCause(raw) {
171
+ const text = String(raw ?? '');
172
+ if (!text.trim()) return 'unknown';
173
+ // FIRST, because an organization's refusal mentions the subscription and
174
+ // reads as a signed-out login to the list below.
175
+ if (ORG_BLOCKED.some((re) => re.test(text))) return 'org-blocked';
176
+ if (SIGNED_OUT.some((re) => re.test(text))) return 'signed-out';
177
+ if (AT_LIMIT.some((re) => re.test(text))) return 'at-limit';
178
+ if (WORKSPACE.some((re) => re.test(text))) return 'workspace';
179
+ // LAST ON PURPOSE, so every text that already had a name keeps it. A dropped
180
+ // connection is the weakest of the four claims: an account that is signed out
181
+ // or capped often mentions a connection on the way down, and the account is
182
+ // the fact worth saying.
183
+ if (INTERRUPTED.some((re) => re.test(text))) return 'interrupted';
184
+ return 'unknown';
185
+ }
186
+
187
+ // WHEN THE LIMIT LETS GO, straight off the line the CLI printed.
188
+ //
189
+ // carries the one thing she actually wants to know, and we were throwing it
190
+ // away and then telling her the vaguer "when the limit resets". The timezone
191
+ // is dropped rather than shown: this Mac is set to America/Los_Angeles and
192
+ // every limit line in her store prints that same zone, so naming it would only
193
+ // add a word she has to read. Null when there is no time to quote, and then
194
+ // the sentence falls back to the wording that has always been there.
195
+ export function limitResetsAt(raw) {
196
+ // Both wordings are real: the CLI's own line is "resets 6pm" and the API's
197
+ // is "Your weekly limit resets at 4pm", so the "at" is optional.
198
+ const m = /resets\s+(?:at\s+)?(\d{1,2}(?::\d{2})?\s*(?:am|pm))/i.exec(String(raw ?? ''));
199
+ return m ? m[1].replace(/\s+/g, '').toLowerCase() : null;
200
+ }
201
+
202
+ const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
203
+
204
+ // THE SAME HOUR AS A MOMENT, so the fleet can wait exactly until it
205
+ // (2026-09-22: a limit that reset at 6:50pm held every agent until 7:03pm,
206
+ // because the app waited a fixed thirty minutes instead of reading this line).
207
+ // The next local occurrence of that clock time at or after `from`; null when
208
+ // the line names no hour.
209
+ export function limitResetMoment(raw, from = Date.now()) {
210
+ // CODEX NAMES A DAY, NOT AN HOUR: "try again at Sep 28th, 2026 1:05 PM"
211
+ // (codex-cli 0.153.4, 2026-09-24). A weekly limit read as "thirty minutes"
212
+ // put the capped login back in the pool every half hour for four days.
213
+ const dated = /try again at ([a-z]{3,9})\.? (\d{1,2})(?:st|nd|rd|th)?,? (\d{4}),? (\d{1,2}):(\d{2})\s*(am|pm)/i.exec(String(raw ?? ''));
214
+ if (dated) {
215
+ const month = MONTHS.indexOf(dated[1].slice(0, 3).toLowerCase());
216
+ if (month >= 0) {
217
+ const h = (Number(dated[4]) % 12) + (dated[6].toLowerCase() === 'pm' ? 12 : 0);
218
+ return new Date(Number(dated[3]), month, Number(dated[2]), h, Number(dated[5]), 0, 0).getTime();
219
+ }
220
+ }
221
+ const hour = limitResetsAt(raw);
222
+ if (!hour) return null;
223
+ const m = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/.exec(hour);
224
+ if (!m) return null;
225
+ const h = (Number(m[1]) % 12) + (m[3] === 'pm' ? 12 : 0);
226
+ const d = new Date(from);
227
+ d.setHours(h, Number(m[2] ?? 0), 0, 0);
228
+ if (d.getTime() < from - 60_000) d.setDate(d.getDate() + 1);
229
+ return d.getTime();
230
+ }
231
+
232
+ // AN ACCOUNT THAT CANNOT BE USED AT ALL UNTIL A PERSON DOES SOMETHING.
233
+ // Worth its own question because it is the one cause where trying again in
234
+ // thirty minutes cannot possibly help, so the account leaves the rotation on
235
+ // the first sighting rather than after three.
236
+ export function needsHerHands(cause) {
237
+ // Both of these are true until a PERSON acts, and neither of them is the
238
+ // same person: one is her, in a terminal; one is an admin of a workspace she
239
+ // may not run. What they share is the only thing this question asks — trying
240
+ // again in thirty minutes cannot possibly help.
241
+ return cause === 'signed-out' || cause === 'org-blocked';
242
+ }
243
+
244
+ // WHAT SHE READS WHEN NOT ONE AGENT CAN START ANYWHERE.
245
+ // Not "an error occurred". The subject of the sentence is the thing she cares
246
+ // about, which is her agents, and the reason follows it in plain words.
247
+ export function troubleSentence(cause) {
248
+ if (cause === 'signed-out') return 'No agents can start. Your Claude login has run out.';
249
+ if (cause === 'org-blocked') return 'No agents can start. Your organization has turned Claude Code off.';
250
+ if (cause === 'at-limit') return 'No agents can start. Your Claude plan is at its limit.';
251
+ if (cause === 'workspace') return 'No agents can start. Claude will not run in this folder.';
252
+ if (cause === 'interrupted') return 'No agents can start. The connection to Claude keeps dropping.';
253
+ return 'No agents can start on this Mac right now.';
254
+ }
255
+
256
+ // THE SECOND LINE: what happens next, so the first line is never a dead end.
257
+ // A sentence that only names a problem makes her go and find out whether she
258
+ // is meant to do something; these say it.
259
+ export function troubleRemedy(cause) {
260
+ // ). "Sign in to Claude again" is a thing to go and work out; /login is a
261
+ // thing to type.
262
+ if (cause === 'signed-out') return 'Open Claude in a terminal, type /login, and they pick straight back up.';
263
+ if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
264
+ if (cause === 'at-limit') return 'They start again on their own when it resets.';
265
+ if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
266
+ if (cause === 'interrupted') return `Nothing to do. ${Name} tries them again on its own.`;
267
+ return `${Name} keeps trying on its own.`;
268
+ }
269
+
270
+ // THE SAME FACT ABOUT ONE ACCOUNT, on the Accounts page, where the reader is
271
+ // already looking at a list of accounts and does not need the word again.
272
+ export function accountSentence(cause, dir) {
273
+ // The folder comes in because the row that shows this sentence REPLACES the
274
+ // folder line with it, so "with this folder set" pointed at something that
275
+ // was no longer on the screen. With the path in the sentence the whole
276
+ // instruction is one line she can retype.
277
+ if (cause === 'signed-out') {
278
+ // Home shortened to ~, the way the rail and the onboarding already draw a
279
+ // folder, so the command fits the row instead of breaking across two lines
280
+ // in the middle of the path. Verified in zsh: the tilde still expands in a
281
+ // command-prefix assignment, so the line stays something she can retype.
282
+ const short = dir ? dir.replace(/^\/Users\/[^/]+/, '~') : null;
283
+ return short
284
+ ? `Signed out. In a terminal run CLAUDE_CONFIG_DIR=${short} claude, then type /login.`
285
+ : 'Signed out. Open Claude in a terminal and type /login.';
286
+ }
287
+ // IT SAYS "SIGNED IN" OUT LOUD, because that is the sentence she came to this
288
+ // page to argue with. No command rides with this one: there is nothing here
289
+ // she can type, and offering one is how she spent an afternoon logging in.
290
+ if (cause === 'org-blocked') {
291
+ return 'Signed in, but its organization has turned Claude Code off. Only an admin of that workspace can turn it back on.';
292
+ }
293
+ if (cause === 'at-limit') return 'At its limit. It rejoins on its own when the limit resets.';
294
+ if (cause === 'workspace') return 'Claude would not run in the folder this was asked to work in.';
295
+ if (cause === 'interrupted') return `Its runs keep getting cut off. ${Name} is trying them again.`;
296
+ return `Its sessions are dying on arrival. ${Name} is keeping the other one busy.`;
297
+ }
298
+
299
+ /* ------------------ the same fact about a whole ENGINE -------------------- */
300
+ // WHAT THE SECOND CODING AGENT'S CARD SAYS WHEN NOTHING WILL RUN ON IT.
301
+ //
302
+ // THE HOLE THIS FILLS. Trouble is remembered per ACCOUNT, keyed through
303
+ // `Supervisor#_accountKey`, which prefixes the second engine because both
304
+ // engines call their primary login 'default'. So a dead Codex spawn is filed at
305
+ // `codex:default`, `status` exports that key verbatim, and the only reader --
306
+ // the Accounts page -- indexes by a bare Claude folder name and can never match
307
+ // it. The fleet-wide sentences above cannot speak for Codex either: the brake
308
+ // they belong to is armed only by a Claude exit with no healthy Claude account
309
+ // left, deliberately, because Codex is the second engine and a row it refuses
310
+ // still runs. So a lapsed Codex subscription stopped every Codex task and the
311
+ // only place the fact existed was the individual rows.
312
+ //
313
+ // WHY IT IS NOT `accountSentence` WITH A DIFFERENT WORD. That one is about ONE
314
+ // ACCOUNT among several on a page listing them, and its signed-out line hands
315
+ // her `CLAUDE_CONFIG_DIR=... claude` -- a command for the other engine, naming a
316
+ // folder Codex does not have. This is about an ENGINE, said on the engine's own
317
+ // card, and it deliberately says nothing about who is signed in: `auth.json`
318
+ // carries an opaque account id and no email, and a uuid is not a person.
319
+ //
320
+ // THE COMMAND IS THE REAL ONE. `codex login` is codex-cli 0.148.0's own
321
+ // subcommand, read off `codex --help` on this Mac 2026-09-05, for the same
322
+ // reason /login is named next door: "sign in again" is a thing to go and work
323
+ // out; a command is a thing to type.
324
+ export function engineTroubleNote(cause, engineWord = 'Codex') {
325
+ if (cause === 'signed-out') {
326
+ return `${engineWord} is signed out, so nothing runs on it. Open a terminal, run codex login, and its tasks pick straight back up.`;
327
+ }
328
+ // A LIMIT IS NOT A THING SHE HAS TO FIX, and saying it is would be worse than
329
+ // saying nothing: it clears itself. Same distinction `troubleRemedy` draws.
330
+ if (cause === 'at-limit') return `${engineWord} is at its usage limit, so its tasks are waiting. They start again on their own when it resets.`;
331
+ if (cause === 'workspace') return `${engineWord} would not run in the folder a task asked to work in. Open that folder in ${engineWord} once and allow it.`;
332
+ if (cause === 'interrupted') return `${engineWord} runs keep getting cut off. ${Name} is trying them again on its own.`;
333
+ return `${engineWord} sessions are dying on arrival, so its tasks are not moving. ${Name} keeps trying.`;
334
+ }
335
+
336
+ /* ------------------ the line above the list (her look four) --------------- */
337
+ // WHAT THE LINE ABOVE HER INBOX SAYS WHEN TASKS ARE NOT RUNNING.
338
+ //
339
+ // Four was this line, said once for the whole app in the surface it already
340
+ // owns, with the silent rows left as they are: "8 tasks have not been able to
341
+ // run since yesterday. Nothing has started on any of them."
342
+ //
343
+ // The count is the point, and it is why this is a different sentence from
344
+ // `troubleSentence` above rather than a rewording of it. That one is about the
345
+ // FLEET, and it can only ever be true when every account on the Mac is down at
346
+ // once. That is why her screen stayed quiet: four Codex tasks and four on a
347
+ // signed-out login sat there all day while her other agents worked, so the
348
+ // fleet was never down and the one line the app owns was structurally unable to
349
+ // speak. The subject here is her TASKS, which is the thing she went looking for
350
+ // and the thing that was actually stuck.
351
+
352
+ // WHEN IT STARTED, IN WORDS, and nothing at all when it started just now.
353
+ // Under three hours the clause is noise, because everything in her inbox is
354
+ // from today. Past that it carries the weight of the sentence: "since
355
+ // yesterday" is the difference between a blip and a day lost.
356
+ export function sinceWords(since, now = Date.now()) {
357
+ if (!since || !Number.isFinite(since) || since > now) return '';
358
+ if (now - since < 3 * 60 * 60_000) return '';
359
+ const then = new Date(since);
360
+ const today = new Date(now);
361
+ const midnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
362
+ if (since >= midnight) {
363
+ // Her Mac's own clock, written the way a person says it out loud: 1:48pm.
364
+ const h = then.getHours();
365
+ const hour = h % 12 === 0 ? 12 : h % 12;
366
+ const mins = String(then.getMinutes()).padStart(2, '0');
367
+ return ` since ${hour}:${mins}${h < 12 ? 'am' : 'pm'}`;
368
+ }
369
+ if (since >= midnight - 24 * 60 * 60_000) return ' since yesterday';
370
+ if (since >= midnight - 6 * 24 * 60 * 60_000) {
371
+ return ` since ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][then.getDay()]}`;
372
+ }
373
+ return ' for over a week';
374
+ }
375
+
376
+ // THE FACT, in her nouns: tasks, run, started. No engine, no account, no folder.
377
+ export function strandedSentence({ count, since, now = Date.now() } = {}) {
378
+ const n = Math.max(0, Math.trunc(count ?? 0));
379
+ return `${strandedTitle(n)}${sinceWords(since, now)}.`
380
+ + ` Nothing has started on ${n !== 1 ? 'any of them' : 'it'}.`;
381
+ }
382
+
383
+ // THE SAME FACT WITH NOTHING AFTER IT, which is what the row carrying it is
384
+ // titled. Here rather than in the renderer so the row, the sentence above and
385
+ // anything else that ever says this cannot drift into three ways of counting
386
+ // the same tasks. One task has, eleven tasks have.
387
+ export function strandedTitle(count) {
388
+ const n = Math.max(0, Math.trunc(count ?? 0));
389
+ const many = n !== 1;
390
+ return `${n} task${many ? 's' : ''} ${many ? 'have' : 'has'} not been able to run`;
391
+ }
392
+
393
+ // THE SECOND LINE: what happens next, so the first is never a dead end.
394
+ //
395
+ // One cause gets the fix for that cause, which is the sentence the app already
396
+ // uses when the whole fleet is down. Two or more and there is no single fix to
397
+ // name, so it says that instead of naming the commonest one and leaving her to
398
+ // find out it only mended half of them. Each row carries its own reason
399
+ // (`deadRunSentence`), which is where that detail belongs and where it fits.
400
+ export function strandedRemedy(causes = []) {
401
+ const distinct = [...new Set(causes)];
402
+ if (distinct.length === 1) return troubleRemedy(distinct[0]);
403
+ return 'They are not all stuck on the same thing. Each row says which.';
404
+ }
405
+
406
+ /* ----------------------- a run that said nothing at all ------------------- */
407
+ // WHAT GOES ON HER ROW WHEN A WORKER DIED WITHOUT WRITING A WORD.
408
+ //
409
+ // It happened blindly because nothing anywhere was allowed to say it. A worker
410
+ // writes its own status through the store, so a worker that dies before it
411
+ // writes leaves the row exactly as it was: still open, still in her inbox,
412
+ // indistinguishable from one nobody has picked up yet. The supervisor knew
413
+ // every time — it counted the runs in `fruitless` — and the count never left
414
+ // the process. Measured on her store that day: six Codex tasks, 18 spawns, all
415
+ // dead inside about a second, zero words on any of the six rows. Two of them
416
+ // sat there a full day.
417
+ //
418
+ // So this sentence is written ONTO THE ROW by the supervisor itself, which is
419
+ // the one reader that survives the worker. It is deliberately about her task
420
+ // and not about our machinery: no exit codes, no engine internals, no paths.
421
+ //
422
+ // `runs` is how many times this row has now been tried and moved nothing, and
423
+ // it is said out loud from the second try, because "it happened again" is the
424
+ // part that tells her this is not a blip.
425
+ // THE FIVE OPENERS, ONE PER CAUSE, WHERE BOTH DIRECTIONS CAN READ THEM.
426
+ //
427
+ // They were five branches inside the sentence below, which was fine while
428
+ // nothing ever had to read one back. `deadRunCause` at the foot of this file
429
+ // does: a row carrying one of these sentences is a row NOTHING RAN ON, and the
430
+ // count above her list has to be able to tell that from a row a worker
431
+ // answered. Writing the words out twice would be two vocabularies waiting to
432
+ // drift apart, so there is one map and the sentence is built off it.
433
+ //
434
+ // FIRST SENTENCE UNDER 112 CHARACTERS, because that is where her inbox row
435
+ // clips (SUMMARY_BUDGET in renderer/src/list-rules.ts) and this is the whole
436
+ // point of the message. Longest of the five is 96 with "Claude Code" in front.
437
+ const DEAD_RUN_OPENERS = {
438
+ workspace: "would not start in this project's folder, so nothing has been done here.",
439
+ 'signed-out': 'could not sign in, so nothing has been done on this task.',
440
+ 'org-blocked': 'is switched off for that account by its organization, so nothing has been done.',
441
+ 'at-limit': 'is at its usage limit, so nothing has been done on this task.',
442
+ interrupted: 'was cut off in the middle, so nothing has been done on this task.',
443
+ unknown: 'stopped before it did anything, so this task has not been started.',
444
+ };
445
+
446
+ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null } = {}) {
447
+ const opener = `${engineWord} ${DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown}`;
448
+
449
+ const again = runs > 1 ? ` This has happened ${runs} times on this task.` : '';
450
+
451
+ // THE TIME THE LIMIT PRINTED, when there is one. "when the limit resets"
452
+ // leaves her with a question; "at 6pm" is an answer, and it is the CLI's own
453
+ // word for it rather than anything we worked out.
454
+ const next = cause === 'signed-out'
455
+ ? ' Sign that account back in and it picks straight up.'
456
+ : cause === 'org-blocked'
457
+ ? ' Only an admin of that Claude workspace can turn it back on.'
458
+ : cause === 'at-limit'
459
+ ? (resetsAt ? ` It starts again on its own at ${resetsAt}.` : ' It starts again on its own when the limit resets.')
460
+ : cause === 'interrupted'
461
+ ? ` ${Name} runs it again on its own.`
462
+ : ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`;
463
+
464
+ return `${opener}${again}${next}`;
465
+ }
466
+
467
+ /* ------------- a run that finished and still delivered nothing ------------ */
468
+ // WHAT GOES ON HER ROW WHEN THE RUN WAS FINE AND HER ANSWER STILL WENT NOWHERE.
469
+ //
470
+ // `settleDelivery` is two independent tests joined with an AND: did the process
471
+ // exit cleanly, and did an agent leave a word on the row since she wrote. A run
472
+ // can pass the first and fail the second, and several ordinary ones do -- a
473
+ // Codex turn that talks in its thread and never calls a store tool, a worker
474
+ // that claims a row and finishes without writing, any session on an install
475
+ // that ships no store MCP at all. That is precisely what the three delivery
476
+ // attempts exist for.
477
+ //
478
+ // But the writer at the end of those three attempts was `sayTheRunDied`, and it
479
+ // is gated on `saidNothingSheCanUse`, which is FALSE for a clean exit. So the
480
+ // third failure returned 'given up' and said nothing: the answered open row was
481
+ // neither settled nor handed back to her with a word on it, and it reads
482
+ // exactly like a row nobody has got to yet, forever, after she did something.
483
+ //
484
+ // IT IS NOT THE DEAD-RUN SENTENCE AND MUST NOT BE. Those five openers all say
485
+ // nothing ran; something ran here. Keeping this out of `DEAD_RUN_OPENERS` is
486
+ // also what keeps `deadRunCause` answering null for it, so a row carrying this
487
+ // is not counted among the ones nothing has started -- which would be a second
488
+ // wrong thing said about the same row.
489
+ //
490
+ // FIRST SENTENCE UNDER 112 CHARACTERS, the same budget the openers keep, because
491
+ // that is where her inbox row clips (SUMMARY_BUDGET, renderer/src/list-rules.ts).
492
+ export function undeliveredAnswerSentence({ engineWord = 'The agent', attempts = 1 } = {}) {
493
+ const n = Math.max(1, Math.trunc(attempts ?? 1));
494
+ const runs = n === 1 ? 'once and finished' : `${n} times and each run finished`;
495
+ return `Your reply did not reach this task. ${engineWord} ran ${runs} without writing anything back on the row, so ${NAME} has stopped carrying it. Reply again and it goes to a fresh session.`;
496
+ }
497
+
498
+ // WHOSE SENTENCE IS ON THIS ROW, AND WHAT IT SAYS HAPPENED.
499
+ //
500
+ // It could not come up. The count behind that row is taken in
501
+ // `sayItOnEveryStrandedRow`, which skips any row it has already spoken on, and
502
+ // the row is normally spoken on by `sayTheRunDied` at the moment of the exit
503
+ // instead. The pass then saw a result newer than the run and read it as
504
+ // somebody having answered, which is exactly what it should read a WORKER'S
505
+ // result as, and it had no way to tell that the result was ours. Measured on
506
+ // her machine that night: 65 rows with dead runs recorded, 10 spoken on, and
507
+ // not one carrying a reason, so the count was zero every tick of its life.
508
+ //
509
+ // So a result is asked whose it is. Null means somebody wrote a real answer
510
+ // there and the row is not stranded; a cause means the last word on that row is
511
+ // still the app saying nothing ran.
512
+ export function deadRunCause(text) {
513
+ const s = String(text ?? '');
514
+ if (!s.trim()) return null;
515
+ for (const [cause, opener] of Object.entries(DEAD_RUN_OPENERS)) {
516
+ if (s.includes(opener)) return cause;
517
+ }
518
+ return null;
519
+ }
520
+
521
+ /* ---------------- one line per cause, for the looks above the list -------- */
522
+ // WHY A BREAKDOWN EXISTS AT ALL.
523
+ //
524
+ // `strandedSentence` above says ONE number for everything, and `strandedRemedy`
525
+ // gives up the moment two causes are in the pile: "They are not all stuck on
526
+ // the same thing. Each row says which." That is the app declining to say what
527
+ // the failures are in the one place she says they belong. On the evening she
528
+ // filed this row the pile was six at a limit that cleared itself in four
529
+ // minutes and four that could not clear themselves at all, and that sentence
530
+ // covered both with the same shrug.
531
+ //
532
+ // So the count is kept per cause. Every look that says more than one thing above
533
+ // the list is drawn off this, and no look invents a word: the phrases are here,
534
+ // once, so two surfaces cannot end up with two vocabularies.
535
+
536
+ // THE SHORT NAME OF THE CAUSE, in her nouns. Never the engine's words. Used
537
+ // where the shape has no room for a sentence, which is most of them.
538
+ export function causeWord(cause) {
539
+ if (cause === 'signed-out') return 'a signed-out login';
540
+ if (cause === 'org-blocked') return 'an organization that blocks Claude Code';
541
+ if (cause === 'at-limit') return 'your Claude limit';
542
+ if (cause === 'workspace') return 'a folder Claude will not run in';
543
+ if (cause === 'interrupted') return 'runs getting cut off';
544
+ return 'something we cannot name';
545
+ }
546
+
547
+ // WHAT IT IS, with the count in front of it, as one clause. The verb is chosen
548
+ // per cause because "waiting" is true of a limit and false of a login that has
549
+ // run out.
550
+ export function causeWhat(cause, count) {
551
+ const n = Math.max(0, Math.trunc(count ?? 0));
552
+ if (cause === 'signed-out') return `${n} stopped by a signed-out Claude login`;
553
+ if (cause === 'org-blocked') return `${n} stopped by an organization that blocks Claude Code`;
554
+ if (cause === 'at-limit') return `${n} waiting on your Claude limit`;
555
+ if (cause === 'workspace') return `${n} in a folder Claude will not run in`;
556
+ if (cause === 'interrupted') return `${n} cut off mid-run`;
557
+ return `${n} stopped for a reason we cannot name`;
558
+ }
559
+
560
+ // WHAT HAPPENS NEXT, so no line on any of these looks is a dead end. The hour
561
+ // comes from the limit line the CLI printed, never from our own clock.
562
+ export function causeNext(cause, resetsAt = null) {
563
+ if (cause === 'signed-out') return 'Type /login in a terminal and they pick straight up.';
564
+ if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
565
+ if (cause === 'at-limit') return resetsAt ? `They start again on their own at ${resetsAt}.` : 'They start again on their own when it resets.';
566
+ if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
567
+ if (cause === 'interrupted') return `Nothing to do. ${Name} is trying them again.`;
568
+ return `${Name} keeps trying on its own.`;
569
+ }
570
+
571
+ // THE ONE THING SHE COULD PRESS, or null when there is honestly nothing. Only
572
+ // the causes a person can end are given a word; a limit is not one of them, and
573
+ // putting a button on it would be the app pretending she has a move.
574
+ export function causeAct(cause) {
575
+ if (cause === 'signed-out') return 'Sign in';
576
+ if (cause === 'workspace') return 'Trust the folder';
577
+ return null;
578
+ }
579
+
580
+ // THE PILE, GROUPED. Biggest first, and a cause she can end always outranks one
581
+ // she cannot at the same count, because the list is read top down and the top
582
+ // of it should be the part with a move in it.
583
+ export function causeBreakdown(rows = [], resetsAt = null) {
584
+ const by = new Map();
585
+ for (const r of rows) {
586
+ const cause = r?.cause ?? 'unknown';
587
+ by.set(cause, (by.get(cause) ?? 0) + 1);
588
+ }
589
+ return [...by.entries()]
590
+ .map(([cause, count]) => ({
591
+ cause,
592
+ count,
593
+ what: causeWhat(cause, count),
594
+ next: causeNext(cause, cause === 'at-limit' ? resetsAt : null),
595
+ act: causeAct(cause),
596
+ hers: needsHerHands(cause) || cause === 'workspace',
597
+ }))
598
+ .sort((a, b) => (b.hers - a.hers) || (b.count - a.count) || a.cause.localeCompare(b.cause));
599
+ }
@@ -0,0 +1 @@
1
+ export function commandRunning(processName: string, exited: boolean): boolean;
@@ -0,0 +1,58 @@
1
+ // IS THERE A COMMAND TO STOP, or is this shell just sitting at its prompt.
2
+ //
3
+ // Both buttons were on the toolbar at all times, side by side, in the same
4
+ // weight, and neither said what it acted on. One sends Ctrl+C to whatever is
5
+ // running; the other kills the shell and everything under it.
6
+ //
7
+ // Half the confusion goes away by itself once "Stop" is only on screen when
8
+ // there is something to stop, because most of the time she looks at the
9
+ // toolbar nothing is running and there is only one button there. The other
10
+ // half is the wording, which now names the object: a COMMAND is stopped, a
11
+ // SHELL is ended.
12
+ //
13
+ // IT LIVES IN `shared` BECAUSE BOTH SIDES ASK IT NOW. The toolbar asks to
14
+ // decide whether to draw Stop command; the main process asks to decide whether
15
+ // a terminal nobody is watching may be ended. Two copies of this rule would
16
+ // mean a shell the toolbar calls busy and the reaper calls idle, which is the
17
+ // one way this feature can lose somebody's work.
18
+ //
19
+ // WHAT THE BACKEND GIVES US is node-pty's `pty.process`, the name of the
20
+ // foreground process group's leader, read off the tty (main/task-terminals.mjs
21
+ // line 50). At an idle prompt that is the shell itself, which macOS reports
22
+ // with a leading dash for a login shell because the app spawns `$SHELL -l`.
23
+ // While `npm test` runs it is `npm`, while `vim` is up it is `vim`.
24
+ //
25
+ // So "idle" is "the name is a shell's name", and everything else is a command.
26
+ // An interactive program whose name happens to be one of these (somebody types
27
+ // `bash` inside zsh) reads as idle and hides the Stop button; Ctrl+C in the
28
+ // terminal itself still works, which is the route anybody who nests a shell
29
+ // already uses.
30
+ const SHELLS = /^(zsh|bash|sh|fish|ksh|dash|tcsh|csh|Shell)$/i;
31
+
32
+ // AND NODE-PTY SAYS THE SAME SHELL THREE DIFFERENT WAYS, WHICH IS WHAT MADE THE
33
+ // FIRST VERSION OF THIS SILENTLY WRONG (measured 2026-09-21 against real ttys,
34
+ // scripts/evaluate-shell-reaping.mjs). The same idle zsh answered `zsh` on one
35
+ // session and `/bin/zsh` on another, and a login shell is `-zsh` because the
36
+ // app spawns `$SHELL -l`. Matched whole, `/bin/zsh` is not in the list above,
37
+ // so that shell read as BUSY: the Stop command button would sit on an idle
38
+ // prompt, and the sweep that ends idle terminals would never end it. The
39
+ // feature would have looked shipped and done nothing.
40
+ //
41
+ // So the name is reduced to what it actually is before it is asked about: no
42
+ // directory, no login dash.
43
+ function shellName(raw) {
44
+ const bare = String(raw).replace(/^-+/, '');
45
+ return (bare.split('/').pop() || bare).replace(/^-+/, '');
46
+ }
47
+
48
+ export function commandRunning(processName, exited) {
49
+ if (exited) return false;
50
+ const name = String(processName || '').trim();
51
+ // NOTHING IS NOT A COMMAND. The backend defaults this to 'Shell' and the
52
+ // component starts it there too, so an empty name should not reach here; if
53
+ // one ever does it means the poll has not answered yet, and a button that
54
+ // appears for a moment on every open and then leaves is worse than one that
55
+ // waits for the first real reading.
56
+ if (!name) return false;
57
+ return !SHELLS.test(shellName(name));
58
+ }