agentbox-app 0.1.12 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -61,12 +61,53 @@
61
61
  // to her subscription and never to a key (CLAUDE.md), so passing that advice on
62
62
  // would be this app telling her to do the one thing it refuses to do.
63
63
  import { NAME, Name } from '../shared/product-name.mjs';
64
+ import { harnessDefinition } from './harness-definitions.mjs';
65
+ import { instantIn, resetDay, wallIn } from './usage.mjs';
66
+
67
+ // A CLAUDE CODE TOO OLD FOR THE LINE AGENTBOX STARTS IT WITH. Its argument
68
+ // parser refuses the line before a single request, in one of two shapes,
69
+ // measured on npm releases 2.1.70 back to 1.0.90 (2026-10-15):
70
+ //
71
+ // error: option '--permission-mode <mode>' argument 'auto' is invalid. ...
72
+ // error: unknown option '--effort'
73
+ //
74
+ // ONLY FLAGS THE APP ITSELF SENDS, and only the mode value it sends. Somebody's
75
+ // own session arguments can carry a typo, and an update does not fix a typo,
76
+ // so an unknown flag of theirs stays 'unknown'. Anchored to the start of a
77
+ // line because that is where the CLI prints it, and an agent quoting the same
78
+ // words from some other program's output is not this.
79
+ const CLAUDE = harnessDefinition('claude');
80
+ const OUR_FLAGS = [
81
+ ...JSON.stringify(CLAUDE.run).match(/--[A-Za-z-]+/g),
82
+ '--allowedTools', '--disallowedTools', '--permission-mode', '--mcp-config', '--permission-prompt-tool', '--settings', '--model',
83
+ ];
84
+ const TOO_OLD = [
85
+ new RegExp(`^error: unknown option '(${OUR_FLAGS.join('|')})'`, 'm'),
86
+ /^error: option '--permission-mode <mode>' argument 'auto' is invalid/m,
87
+ ];
64
88
  const ORG_BLOCKED = [
65
89
  /organi[sz]ation has disabled/i,
66
90
  /disabled for (your|this) organi[sz]ation/i,
67
91
  /ask your admin/i,
68
92
  ];
69
93
 
94
+ // A CODEX SIGNED IN WITH AN API KEY. Our own line, said before the worker
95
+ // starts (main/codex-account.mjs `codexKeyRefusal`), because Codex reads the
96
+ // key out of auth.json past the worker scrub and bills every call to it.
97
+ // Before SIGNED_OUT, whose /invalid api key/ is a different event.
98
+ const API_KEY = [/signed in with an api key/i];
99
+
100
+ // A CODEX config.toml CODEX WILL NOT READ. Codex's own words, off real runs of
101
+ // codex-cli 0.160.0 on 2026-10-15: the app-server refusing `config/read` for a
102
+ // quote left open or an old `profile = "..."` line, and the terminal's
103
+ // "Error loading config.toml". Not benched like a login: fixing the file
104
+ // changes no sign-in file, so a benched account would never come back.
105
+ const CODEX_CONFIG = [
106
+ /refused config\/read: failed to (read configuration layers|resolve feature override)/i,
107
+ /error loading config\.toml/i,
108
+ /config\.toml:\d+:\d+: /i,
109
+ ];
110
+
70
111
  const SIGNED_OUT = [
71
112
  /oauth/i,
72
113
  /session expired/i,
@@ -133,7 +174,11 @@ const INTERRUPTED = [
133
174
  /connection (closed|lost|dropped)/i,
134
175
  /response stopped arriving/i,
135
176
  /may be incomplete/i,
136
- /econnreset|enotfound|etimedout|econnrefused/i,
177
+ /econnreset|enotfound|etimedout|econnrefused|enetunreach|ehostunreach|eai_again/i,
178
+ // Codex also prefixes non-network failures with "stream disconnected".
179
+ // Match the request failure itself so incomplete responses keep their limit.
180
+ /error sending request for url/i,
181
+ /failed to send websocket request/i,
137
182
  /unable to connect/i,
138
183
  /can.{0,3}t reach the api/i,
139
184
  /timed out/i,
@@ -141,6 +186,34 @@ const INTERRUPTED = [
141
186
  /overloaded/i,
142
187
  ];
143
188
 
189
+ // A NETWORK THAT WILL NOT LET A SECURE CONNECTION THROUGH (2026-10-15).
190
+ //
191
+ // A company network that inspects TLS swaps in its own certificate, and a
192
+ // proxy can ask for a login. Neither clears by waiting, which is what made
193
+ // 'interrupted' the wrong home for them: the real Claude Code 2.1.301 behind a
194
+ // self-signed proxy died in 371 ms with "API Error: Unable to connect to API:
195
+ // Self-signed certificate detected (DEPTH_ZERO_SELF_SIGNED_CERT) ...", "unable
196
+ // to connect" filed it as a dropped connection, and the screen said there was
197
+ // nothing to do. Node's own spellings, Codex's (rustls and OpenSSL) and a
198
+ // proxy's 407 are here too. A REFUSED connection is not: "Connection refused
199
+ // — a firewall or proxy may be blocking it (ECONNREFUSED)" is also what an
200
+ // offline laptop prints.
201
+ //
202
+ // The fix is a setting the person's terminal already has, and since
203
+ // main/shell-path.mjs the app reads it from their shell profile at launch, so
204
+ // the sentence says to put it there and reopen the app.
205
+ const NETWORK = [
206
+ /self[- ]signed certificate/i,
207
+ /unable to get local issuer certificate/i,
208
+ /unable to verify the first certificate/i,
209
+ /\b(DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|UNABLE_TO_VERIFY_LEAF_SIGNATURE)\b/,
210
+ /tls_untrusted_ca/i,
211
+ /invalid peer certificate/i,
212
+ /certificate verify failed/i,
213
+ /proxy authentication required/i,
214
+ ];
215
+ const NETWORK_FIX = 'export HTTPS_PROXY and NODE_EXTRA_CA_CERTS in your shell profile';
216
+
144
217
  // A workspace the CLI refuses to run in. Distinct from the two above because
145
218
  // the fix is a folder, not an account.
146
219
  const WORKSPACE = [
@@ -164,17 +237,50 @@ const WORKSPACE = [
164
237
  /skip-git-repo-check/i,
165
238
  ];
166
239
 
240
+ // A MODEL THE LOGIN'S PLAN DOES NOT INCLUDE. Not the account being out and not
241
+ // the model being unknown: the same login runs every other model, and another
242
+ // login on a bigger plan runs this one. Codex's own words, off two dead runs on
243
+ // 2026-10-09 and 2026-10-11 (codex-cli 0.160.0, a free-plan login handed
244
+ // gpt-6.1-sol): "The 'gpt-6.1-sol' model is not supported when using Codex with
245
+ // a ChatGPT account." The other two are ours, thrown before the thread starts
246
+ // by `codexModelRefusal`: this login's list lacks it, or no login's list here
247
+ // has it, which is what a paid plan that has ended looks like.
248
+ const MODEL_NOT_ON_PLAN = [
249
+ /model is not supported when using codex with a chatgpt account/i,
250
+ /not a model the plan of the codex account/i,
251
+ /not a model the codex on this computer knows/i,
252
+ ];
253
+
254
+ // Ours too, from `codexEffortRefusal`.
255
+ const LEVEL_NOT_OFFERED = [/not a level that model offers/i];
256
+
257
+ // Codex lets one process write to a conversation at a time, and refuses our
258
+ // resume while the Codex app or a terminal has it open. codex-cli 0.163.0:
259
+ // "thread <id> already has an active writer".
260
+ const OPEN_ELSEWHERE = [/already has an active writer/i];
261
+
167
262
  // WHICH OF THE THREE THINGS WENT WRONG, from whatever the tool printed.
168
263
  // 'unknown' is a first-class answer, not a failure: an honest generic sentence
169
264
  // beats a confident wrong one.
170
265
  export function troubleCause(raw) {
171
266
  const text = String(raw ?? '');
172
267
  if (!text.trim()) return 'unknown';
268
+ // The CLI's own argument check, before any account is asked anything.
269
+ if (TOO_OLD.some((re) => re.test(text))) return 'too-old';
270
+ if (CODEX_CONFIG.some((re) => re.test(text))) return 'codex-config';
271
+ if (API_KEY.some((re) => re.test(text))) return 'api-key';
173
272
  // FIRST, because an organization's refusal mentions the subscription and
174
273
  // reads as a signed-out login to the list below.
175
274
  if (ORG_BLOCKED.some((re) => re.test(text))) return 'org-blocked';
275
+ // BEFORE SIGNED-OUT, because Codex's sentence says "ChatGPT account".
276
+ if (MODEL_NOT_ON_PLAN.some((re) => re.test(text))) return 'model-not-on-plan';
277
+ if (LEVEL_NOT_OFFERED.some((re) => re.test(text))) return 'level-not-offered';
278
+ if (OPEN_ELSEWHERE.some((re) => re.test(text))) return 'open-elsewhere';
176
279
  if (SIGNED_OUT.some((re) => re.test(text))) return 'signed-out';
177
280
  if (AT_LIMIT.some((re) => re.test(text))) return 'at-limit';
281
+ // BEFORE WORKSPACE, whose /untrusted/ would read a certificate as a folder,
282
+ // and before INTERRUPTED, whose "unable to connect" is in Claude Code's line.
283
+ if (NETWORK.some((re) => re.test(text))) return 'network';
178
284
  if (WORKSPACE.some((re) => re.test(text))) return 'workspace';
179
285
  // LAST ON PURPOSE, so every text that already had a name keeps it. A dropped
180
286
  // connection is the weakest of the four claims: an account that is signed out
@@ -184,22 +290,118 @@ export function troubleCause(raw) {
184
290
  return 'unknown';
185
291
  }
186
292
 
187
- // WHEN THE LIMIT LETS GO, straight off the line the CLI printed.
293
+ // A SUBSCRIPTION THAT HAS ENDED IS REFUSED IN AN ADMIN'S WORDS. Claude Code
294
+ // 2.1.293 told a lapsed personal Max login "Your organization has disabled
295
+ // Claude subscription access for Claude Code" (2026-10-11), the same line a
296
+ // workspace policy prints, so the text alone cannot tell them apart. The
297
+ // login's own file can: `plan` is main/claude-plan.mjs's reading of it, and
298
+ // `ended` is its billing gone to "none". Anything else keeps its cause.
299
+ // A LOGIN THAT RAN OUT AND ONE NOBODY EVER MADE REFUSE IN THE SAME WORDS, and
300
+ // the second is what somebody who skipped setup meets: "Sign that account back
301
+ // in" about an account they never had. `everSignedIn` is the caller's reading
302
+ // of the login's own files; false only where that reading is proof.
303
+ export function refineCause(cause, plan, { everSignedIn = true } = {}) {
304
+ if (cause === 'org-blocked' && plan?.ended) return 'subscription-ended';
305
+ if (cause === 'signed-out' && !everSignedIn) return 'no-plan';
306
+ return cause;
307
+ }
308
+
309
+ // THE CLOCK A LIMIT LINE NAMES, AND THE ZONE IT NAMED IT IN.
310
+ //
311
+ // The zone used to be dropped on the floor, on the assumption that the CLI
312
+ // prints the clock the Mac is already set to. A run on the cloud prints the
313
+ // cloud machine's: "resets 12:10am (UTC)" reached a Mac on Pacific time
314
+ // (2026-10-16), and both the hour on the row and the moment the fleet waited
315
+ // for were seven hours out. `offset` is minutes east of UTC, and null when
316
+ // the line printed no zone at all — which is still most lines, and still
317
+ // means local. This reads any offset, and reads it for the sentence as well
318
+ // as for the bench, which the first fix for that incident did not.
319
+ //
320
+ // Both wordings are real: the CLI's own line is "resets 6pm" and the API's is
321
+ // "Your weekly limit resets at 4pm", so the "at" is optional.
322
+ function resetClock(raw) {
323
+ const m = /resets\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*(?:\(\s*(?:UTC|GMT)\s*(?:([+-])(\d{1,2})(?::?(\d{2}))?)?\s*\))?/i
324
+ .exec(String(raw ?? ''));
325
+ if (!m) return null;
326
+ const zoned = /\(\s*(?:UTC|GMT)/i.test(m[0]);
327
+ return {
328
+ h: (Number(m[1]) % 12) + (m[3].toLowerCase() === 'pm' ? 12 : 0),
329
+ min: Number(m[2] ?? 0),
330
+ offset: zoned ? (m[4] === '-' ? -1 : 1) * (Number(m[5] ?? 0) * 60 + Number(m[6] ?? 0)) : null,
331
+ };
332
+ }
333
+
334
+ // The CLI's own wording for a clock time, so a converted hour reads exactly
335
+ // the way a quoted one always has.
336
+ function clockWords(h, min) {
337
+ return `${h % 12 || 12}${min ? `:${String(min).padStart(2, '0')}` : ''}${h < 12 ? 'am' : 'pm'}`;
338
+ }
339
+
340
+ // "3:15pm" today, "Wed 2:30pm" this week, "Wed Oct 28 1:05pm" further out,
341
+ // the same day words the account meters use.
342
+ function momentWords(ms, from) {
343
+ const d = new Date(ms);
344
+ const day = resetDay(ms, from);
345
+ return `${day ? `${day} ` : ''}${clockWords(d.getHours(), d.getMinutes())}`;
346
+ }
347
+
348
+ // WHEN THE LIMIT LETS GO, IN THE CLOCK SHE IS LOOKING AT.
188
349
  //
189
350
  // 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: the limit line prints the zone the Mac is
192
- // already set to, so naming it would only add a word she has to read. Null when there is no time to quote, and then
193
- // the sentence falls back to the wording that has always been there.
194
- export function limitResetsAt(raw) {
195
- // Both wordings are real: the CLI's own line is "resets 6pm" and the API's
196
- // is "Your weekly limit resets at 4pm", so the "at" is optional.
197
- const m = /resets\s+(?:at\s+)?(\d{1,2}(?::\d{2})?\s*(?:am|pm))/i.exec(String(raw ?? ''));
198
- return m ? m[1].replace(/\s+/g, '').toLowerCase() : null;
351
+ // away and then telling her the vaguer "when the limit resets". A line that
352
+ // names its own zone is converted to this Mac's clock rather than quoted, so
353
+ // the hour on the row is an hour she can act on; a line that names none is
354
+ // already local and is quoted as it stands. Null when there is no time to
355
+ // quote, and then the sentence falls back to the wording that has always been
356
+ // there.
357
+ export function limitResetsAt(raw, from = Date.now()) {
358
+ const clock = resetClock(raw);
359
+ if (!clock) {
360
+ // A reset on another day ("resets Oct 22 at 4pm", Codex's "try again at
361
+ // Oct 28th, 2026 1:05 PM") has no bare hour to quote.
362
+ const moment = limitResetMoment(raw, from);
363
+ return moment == null ? null : momentWords(moment, from);
364
+ }
365
+ if (clock.offset == null) return clockWords(clock.h, clock.min);
366
+ const at = new Date(limitResetMoment(raw, from));
367
+ return clockWords(at.getHours(), at.getMinutes());
199
368
  }
200
369
 
201
370
  const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
202
371
 
372
+ const CLAUDE_DAY = /resets\s+([a-z]{3})[a-z]*\.?\s+(\d{1,2})\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*(?:\(\s*([^)]+?)\s*\))?/i;
373
+ const UTC_ZONE = /^(?:UTC|GMT)\s*(?:([+-])(\d{1,2})(?::?(\d{2}))?)?$/i;
374
+
375
+ // "UTC", "GMT+2" or a place such as "America/Los_Angeles". A zone this
376
+ // computer cannot name, or none, is its own clock.
377
+ function zoneOf(word) {
378
+ const utc = UTC_ZONE.exec(word ?? '');
379
+ if (utc) return { offset: (utc[1] === '-' ? -1 : 1) * (Number(utc[2] ?? 0) * 60 + Number(utc[3] ?? 0)), name: null };
380
+ return { offset: null, name: word && wallIn(Date.now(), word) ? word : null };
381
+ }
382
+
383
+ // The moment a dated Claude Code reset names, or null. The line has no year,
384
+ // so it is the year in that zone at `from`, or the next one when that puts the
385
+ // reset more than a day behind (December reading a January date).
386
+ function claudeResetDay(raw, from) {
387
+ const m = CLAUDE_DAY.exec(String(raw ?? ''));
388
+ if (!m) return null;
389
+ const month = MONTHS.indexOf(m[1].toLowerCase());
390
+ if (month < 0) return null;
391
+ const day = Number(m[2]);
392
+ const h = (Number(m[3]) % 12) + (m[5].toLowerCase() === 'pm' ? 12 : 0);
393
+ const min = Number(m[4] ?? 0);
394
+ const zone = zoneOf(m[6]);
395
+ const at = (year) => zone.offset != null
396
+ ? Date.UTC(year, month, day, h, min) - zone.offset * 60_000
397
+ : instantIn(year, month, day, h, min, zone.name);
398
+ const year = zone.offset != null
399
+ ? new Date(from + zone.offset * 60_000).getUTCFullYear()
400
+ : (wallIn(from, zone.name)?.year ?? new Date(from).getFullYear());
401
+ const moment = at(year);
402
+ return moment < from - 86_400_000 ? at(year + 1) : moment;
403
+ }
404
+
203
405
  // THE SAME HOUR AS A MOMENT, so the fleet can wait exactly until it
204
406
  // (a limit that reset at, say, 6:50pm could hold every agent until 7:03pm,
205
407
  // because the app waited a fixed thirty minutes instead of reading this line).
@@ -217,15 +419,26 @@ export function limitResetMoment(raw, from = Date.now()) {
217
419
  return new Date(Number(dated[3]), month, Number(dated[2]), h, Number(dated[5]), 0, 0).getTime();
218
420
  }
219
421
  }
220
- const hour = limitResetsAt(raw);
221
- if (!hour) return null;
222
- const m = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/.exec(hour);
223
- if (!m) return null;
224
- const h = (Number(m[1]) % 12) + (m[3] === 'pm' ? 12 : 0);
225
- const d = new Date(from);
226
- d.setHours(h, Number(m[2] ?? 0), 0, 0);
227
- if (d.getTime() < from - 60_000) d.setDate(d.getDate() + 1);
228
- return d.getTime();
422
+ // Claude Code names the day when the reset is not today: "resets Oct 22 at
423
+ // 4pm (America/Los_Angeles)".
424
+ const datedReset = claudeResetDay(raw, from);
425
+ if (datedReset != null) return datedReset;
426
+ const clock = resetClock(raw);
427
+ if (!clock) return null;
428
+ if (clock.offset == null) {
429
+ const d = new Date(from);
430
+ d.setHours(clock.h, clock.min, 0, 0);
431
+ if (d.getTime() < from - 60_000) d.setDate(d.getDate() + 1);
432
+ return d.getTime();
433
+ }
434
+ // The named zone's own calendar day is not this Mac's, so the candidate is
435
+ // built in UTC and then walked to the one occurrence that sits in the next
436
+ // twenty-four hours. The grace is for the two clocks disagreeing by seconds.
437
+ const asked = new Date(from);
438
+ let t = Date.UTC(asked.getUTCFullYear(), asked.getUTCMonth(), asked.getUTCDate(), clock.h, clock.min) - clock.offset * 60_000;
439
+ while (t < from - 60_000) t += 86_400_000;
440
+ while (t - from >= 86_400_000) t -= 86_400_000;
441
+ return t;
229
442
  }
230
443
 
231
444
  // AN ACCOUNT THAT CANNOT BE USED AT ALL UNTIL A PERSON DOES SOMETHING.
@@ -237,18 +450,32 @@ export function needsHerHands(cause) {
237
450
  // same person: one is her, in a terminal; one is an admin of a workspace she
238
451
  // may not run. What they share is the only thing this question asks — trying
239
452
  // again in thirty minutes cannot possibly help.
240
- return cause === 'signed-out' || cause === 'org-blocked';
453
+ // An out-of-date Claude Code is hers too: no retry updates it, and every run
454
+ // until she does spends nothing but a reply's tries.
455
+ // A Codex signed in with a key is hers as well, and `codex login` rewrites
456
+ // auth.json, which is what brings a benched account back.
457
+ return cause === 'signed-out' || cause === 'no-plan' || cause === 'org-blocked' || cause === 'subscription-ended' || cause === 'too-old' || cause === 'api-key';
241
458
  }
242
459
 
460
+ // THE ONE COMMAND THAT UPDATES IT, WHEREVER IT CAME FROM. `claude update`
461
+ // updates a native install itself and, on a Homebrew one, prints the brew line
462
+ // to run instead (read in 2.1.80's own code), so it is the right first thing
463
+ // to type whatever she installed it with.
464
+ const UPDATE = `run claude update in a terminal (${Name} needs version ${CLAUDE.minVersion} or newer)`;
465
+
243
466
  // WHAT SHE READS WHEN NOT ONE AGENT CAN START ANYWHERE.
244
467
  // Not "an error occurred". The subject of the sentence is the thing she cares
245
468
  // about, which is her agents, and the reason follows it in plain words.
246
469
  export function troubleSentence(cause) {
247
470
  if (cause === 'signed-out') return 'No agents can start. Your Claude login has run out.';
471
+ if (cause === 'no-plan') return 'No agents can start. No AI plan is set up on this computer.';
248
472
  if (cause === 'org-blocked') return 'No agents can start. Your organization has turned Claude Code off.';
473
+ if (cause === 'subscription-ended') return 'No agents can start. Your Claude subscription has ended.';
249
474
  if (cause === 'at-limit') return 'No agents can start. Your Claude plan is at its limit.';
250
475
  if (cause === 'workspace') return 'No agents can start. Claude will not run in this folder.';
251
476
  if (cause === 'interrupted') return 'No agents can start. The connection to Claude keeps dropping.';
477
+ if (cause === 'network') return 'No agents can start. This network will not let them connect securely.';
478
+ if (cause === 'too-old') return 'No agents can start. Claude Code on this computer is too old.';
252
479
  return 'No agents can start on this computer right now.';
253
480
  }
254
481
 
@@ -259,10 +486,14 @@ export function troubleRemedy(cause) {
259
486
  // ). "Sign in to Claude again" is a thing to go and work out; /login is a
260
487
  // thing to type.
261
488
  if (cause === 'signed-out') return 'Open Claude in a terminal, type /login, and they pick straight back up.';
489
+ if (cause === 'no-plan') return `Set one up ${SET_UP_A_PLAN} and they pick straight up.`;
262
490
  if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
491
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai and they pick straight up.';
263
492
  if (cause === 'at-limit') return 'They start again on their own when it resets.';
264
493
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
265
494
  if (cause === 'interrupted') return `Nothing to do. ${Name} tries them again on its own.`;
495
+ if (cause === 'network') return `If your company uses a proxy or its own certificate, ${NETWORK_FIX} and reopen ${Name}.`;
496
+ if (cause === 'too-old') return `To update it, ${UPDATE}. They start again on their own.`;
266
497
  return `${Name} keeps trying on its own.`;
267
498
  }
268
499
 
@@ -289,10 +520,17 @@ export function accountSentence(cause, dir) {
289
520
  if (cause === 'org-blocked') {
290
521
  return 'Signed in, but its organization has turned Claude Code off. Only an admin of that workspace can turn it back on.';
291
522
  }
292
- if (cause === 'at-limit') return 'At its limit. It rejoins on its own when the limit resets.';
523
+ if (cause === 'subscription-ended') return 'Signed in, but its Claude subscription has ended. Renew it at claude.ai to use this account again.';
524
+ if (cause === 'at-limit') return 'At its limit. Agents start again on their own when it resets.';
293
525
  if (cause === 'workspace') return 'Claude would not run in the folder this was asked to work in.';
294
526
  if (cause === 'interrupted') return `Its runs keep getting cut off. ${Name} is trying them again.`;
295
- return `Its sessions are dying on arrival. ${Name} is keeping the other one busy.`;
527
+ // NOT THE ACCOUNT, and said so, because this is the page where she would
528
+ // otherwise go and sign it in again.
529
+ if (cause === 'network') return `The account is fine; this network will not let it connect securely. Set your company's proxy or certificate in your shell profile and reopen ${Name}.`;
530
+ // Not this account either: every account here runs the same Claude Code.
531
+ if (cause === 'too-old') return `Claude Code on this computer is too old. To update it, ${UPDATE}.`;
532
+ // A cause we cannot name points at the account, not at the app (2026-10-11).
533
+ return 'Claude Code would not start on this account. Check that its plan is active at claude.ai.';
296
534
  }
297
535
 
298
536
  /* ------------------ the same fact about a whole ENGINE -------------------- */
@@ -329,6 +567,9 @@ export function engineTroubleNote(cause, engineWord = 'Codex') {
329
567
  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.`;
330
568
  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.`;
331
569
  if (cause === 'interrupted') return `${engineWord} runs keep getting cut off. ${Name} is trying them again on its own.`;
570
+ if (cause === 'network') return `${engineWord} cannot connect securely through this network, so its tasks are not moving. Set your company's proxy or certificate in your shell profile and reopen ${Name}.`;
571
+ if (cause === 'api-key') return `${engineWord} is signed in with an API key, which ${Name} never bills, so nothing runs on it. Open a terminal, run codex login, and sign in with ChatGPT.`;
572
+ if (cause === 'codex-config') return `${engineWord} could not read its config.toml, so nothing runs on it. Run codex in a terminal to see the line it points at, and fix it.`;
332
573
  return `${engineWord} sessions are dying on arrival, so its tasks are not moving. ${Name} keeps trying.`;
333
574
  }
334
575
 
@@ -437,11 +678,42 @@ const DEAD_RUN_OPENERS = {
437
678
  workspace: "would not start in this project's folder, so nothing has been done here.",
438
679
  'signed-out': 'could not sign in, so nothing has been done on this task.',
439
680
  'org-blocked': 'is switched off for that account by its organization, so nothing has been done.',
681
+ 'subscription-ended': "cannot run because that account's Claude subscription has ended, so nothing has been done.",
440
682
  'at-limit': 'is at its usage limit, so nothing has been done on this task.',
441
683
  interrupted: 'was cut off in the middle, so nothing has been done on this task.',
684
+ 'model-not-on-plan': "could not use this task's model on that account's plan, so nothing has been done.",
685
+ 'level-not-offered': "could not run this task's model at the thinking level it asks for, so nothing has been done.",
686
+ network: 'could not connect securely through this network, so nothing has been done on this task.',
687
+ 'too-old': `on this computer is older than ${Name} needs, so nothing has been done on this task.`,
688
+ 'api-key': `is signed in with an API key, which ${Name} never bills, so nothing has been done.`,
689
+ 'codex-config': 'could not read its config.toml, so nothing has been done on this task.',
690
+ 'open-elsewhere': 'has this conversation open in another window, so nothing has been done here.',
691
+ // THE ONE OPENER WITH NO ENGINE IN FRONT OF IT: somebody who has set nothing
692
+ // up has never heard of Codex, and the plan screens never say the word.
693
+ 'no-plan': 'No AI plan is set up on this computer, so nothing has been done on this task.',
442
694
  unknown: 'stopped before it did anything, so this task has not been started.',
443
695
  };
444
696
 
697
+ // WHERE A PLAN GETS SET UP: the bar across the top of the inbox (`PlanBar`),
698
+ // which is the only way back into setup. Said once, for the four places that
699
+ // point at it.
700
+ const SET_UP_A_PLAN = 'at the top of your inbox';
701
+
702
+ // The same, for a run that used a tool before it died, so "nothing has been
703
+ // done" would be false. Kept out of DEAD_RUN_OPENERS so `deadRunCause` does
704
+ // not count the row among those nothing has started on. A cause missing here
705
+ // can only stop a run before its first tool, so on a run that worked it is a
706
+ // misreading and gets the plain line.
707
+ const WORKED_OPENERS = {
708
+ 'signed-out': 'was signed out partway through, so this task is not finished.',
709
+ 'org-blocked': 'was switched off for that account partway through, so this task is not finished.',
710
+ 'subscription-ended': "stopped partway through because that account's subscription ended.",
711
+ 'at-limit': 'hit its usage limit partway through, so this task is not finished.',
712
+ interrupted: 'was cut off partway through, so this task is not finished.',
713
+ network: 'lost its secure connection partway through, so this task is not finished.',
714
+ unknown: 'stopped partway through and did not finish this task.',
715
+ };
716
+
445
717
  /**
446
718
  * THE LINE IN A THREAD WHERE A RUN STOPPED, in our words.
447
719
  *
@@ -453,10 +725,19 @@ const DEAD_RUN_OPENERS = {
453
725
  */
454
726
  const RUN_STOPPED = {
455
727
  'signed-out': 'This run stopped because the account was signed out.',
456
- 'org-blocked': 'This run stopped because that account is switched off by its organization.',
728
+ // Same refusal text for an admin's block and an ended subscription, and this
729
+ // line has only the text, so it says what is true of both.
730
+ 'org-blocked': 'This run stopped because Claude Code will not run on that account.',
457
731
  'at-limit': 'This run stopped because the account hit its usage limit.',
458
732
  workspace: "This run stopped because it would not start in this project's folder.",
459
733
  interrupted: 'This run was cut off in the middle.',
734
+ 'model-not-on-plan': "This run stopped because that account's plan does not include the model.",
735
+ 'level-not-offered': 'This run stopped because the model does not offer that thinking level.',
736
+ network: 'This run stopped because this network would not let it connect securely.',
737
+ 'too-old': `This run stopped because Claude Code on this computer is older than ${Name} needs.`,
738
+ 'api-key': 'This run stopped because Codex is signed in with an API key.',
739
+ 'codex-config': 'This run stopped because Codex could not read its config.toml.',
740
+ 'open-elsewhere': 'This run stopped because the conversation is open in another Codex window.',
460
741
  unknown: 'This run stopped before it finished.',
461
742
  };
462
743
 
@@ -464,27 +745,135 @@ export function runStoppedLine(raw) {
464
745
  return RUN_STOPPED[troubleCause(raw)] ?? RUN_STOPPED.unknown;
465
746
  }
466
747
 
467
- export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null } = {}) {
468
- const opener = `${engineWord} ${DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown}`;
748
+ // A cause we cannot name still has a likeliest culprit, and it is the agent's
749
+ // plan, not the app (2026-10-19). Engines with no plan to point at get none.
750
+ const UNNAMED_CHECK = {
751
+ Codex: 'Check that your ChatGPT plan is active at chatgpt.com, or run codex in a terminal to see what it says. ',
752
+ 'Claude Code': 'Check that your Claude plan is active at claude.ai, or run claude in a terminal to see what it says. ',
753
+ };
754
+
755
+ function dayWords(ms) {
756
+ return new Date(ms).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
757
+ }
758
+
759
+ // What to do before replying to a thread the app has stopped trying on, in the
760
+ // same words `deadRunSentence` uses for each cause. Null when the reply is all.
761
+ function fixBeforeReplying({ cause, engineWord, resetsAt, model, planEnded }) {
762
+ if (cause === 'signed-out') return 'Sign that account back in.';
763
+ if (cause === 'no-plan') return `Set up a plan ${SET_UP_A_PLAN}.`;
764
+ if (cause === 'org-blocked') return 'Only an admin of that Claude workspace can turn it back on.';
765
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai.';
766
+ if (cause === 'at-limit') return resetsAt ? `The limit resets at ${resetsAt}.` : 'Wait for the limit to reset.';
767
+ if (cause === 'too-old') return `To update it, ${UPDATE}.`;
768
+ if (cause === 'api-key') return 'Run codex login in a terminal to sign in with your ChatGPT plan.';
769
+ if (cause === 'codex-config') return 'Run codex in a terminal to see the line it points at and fix it.';
770
+ if (cause === 'open-elsewhere') return 'Close it there.';
771
+ if (cause === 'model-not-on-plan') {
772
+ return planEnded
773
+ ? `That account's ChatGPT subscription ended on ${dayWords(planEnded)}, and the free plan does not include ${model || 'this model'}. Renew it at chatgpt.com, or pick another model for this task.`
774
+ : 'Pick another model for this task.';
775
+ }
776
+ if (cause === 'level-not-offered') return 'Pick another thinking level for this task.';
777
+ if (cause === 'network') return `If your company uses a proxy or its own certificate, ${NETWORK_FIX} and reopen ${Name}.`;
778
+ if (cause === 'workspace') return `Open this project's folder in ${engineWord} once and say yes to trusting it.`;
779
+ if (cause === 'unknown') return UNNAMED_CHECK[engineWord]?.trim() || null;
780
+ return null;
781
+ }
782
+
783
+ // NO SENTENCE HERE OFFERS ANOTHER ACCOUNT (w-dac993a554). There used to be an
784
+ // `elsewhere` flag: another signed-in login had room, or had the model, so the
785
+ // line promised the move instead of the wait. Nothing moves work between
786
+ // accounts any more, so a limit is named with the hour it resets and a model no
787
+ // plan includes is answered with another model.
788
+ //
789
+ // `planEnded` is when the account's paid ChatGPT plan ended (main/codex-account.mjs
790
+ // `codexPlanEnded`), which is why its list lacks `model`.
791
+ //
792
+ // `gaveUp` is the app having stopped carrying a reply (`settleDeliveryAndSay`).
793
+ // Nothing restarts the thread until somebody replies or resumes it, so the
794
+ // sentence asks for that and promises nothing of its own.
795
+ // `account` is the login this run was spending, already in words
796
+ // (shared/account-names.mjs). It is null on a Mac with one login, where naming
797
+ // it tells nobody anything, and then every sentence here is the one it has
798
+ // always been. There is no `instead`: nothing takes the task to another login
799
+ // (w-dac993a554), so a limit is named and waited out.
800
+ //
801
+ // `partway` is a run that was picking up a thread an earlier run worked on: it
802
+ // may have done nothing itself, but the task has been started. A cause with no
803
+ // partway words stopped this run before it began, and keeps its own opener.
804
+ //
805
+ // `retryAt` is when the rest ladder starts it again (`restingUntil` in
806
+ // main/supervisor.mjs), named only on the causes that wait for nothing else.
807
+ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null, worked: usedATool = false, partway = false, model = null, planEnded = null, gaveUp = false, account = null, retryAt = null, now = Date.now() } = {}) {
808
+ if (usedATool && !WORKED_OPENERS[cause]) cause = 'unknown';
809
+ const worked = usedATool || (partway && !!WORKED_OPENERS[cause]);
810
+ // A LIMIT IS A FACT ABOUT ONE LOGIN, so that is the one cause whose opener
811
+ // takes the name. The others are about the tool or the task.
812
+ const limitOpener = cause === 'at-limit' && account
813
+ ? (worked
814
+ ? `hit its usage limit on ${account} partway through, so this task is not finished.`
815
+ : `is at its usage limit on ${account}, so nothing has been done on this task.`)
816
+ : null;
817
+ const opener = cause === 'no-plan'
818
+ ? DEAD_RUN_OPENERS['no-plan']
819
+ : `${engineWord} ${limitOpener ?? (worked ? WORKED_OPENERS[cause] ?? WORKED_OPENERS.unknown : DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown)}`;
469
820
 
470
821
  const again = runs > 1 ? ` This has happened ${runs} times on this task.` : '';
471
822
 
823
+ if (gaveUp) {
824
+ const fix = fixBeforeReplying({ cause, engineWord, resetsAt, model, planEnded });
825
+ return `${opener}${again} ${fix ? `${fix} Then reply` : 'Reply'} here to start it again.`;
826
+ }
827
+
828
+ const when = retryAt > now ? ` at ${momentWords(retryAt, now)}` : '';
472
829
  // THE TIME THE LIMIT PRINTED, when there is one. "when the limit resets"
473
830
  // leaves her with a question; "at 6pm" is an answer, and it is the CLI's own
474
831
  // word for it rather than anything we worked out.
475
832
  const next = cause === 'signed-out'
476
833
  ? ' Sign that account back in and it picks straight up.'
834
+ : cause === 'no-plan'
835
+ ? ` Set one up ${SET_UP_A_PLAN} and it picks straight up.`
477
836
  : cause === 'org-blocked'
478
837
  ? ' Only an admin of that Claude workspace can turn it back on.'
838
+ : cause === 'subscription-ended'
839
+ ? ' Renew it at claude.ai and it picks straight up.'
479
840
  : cause === 'at-limit'
480
841
  ? (resetsAt ? ` It starts again on its own at ${resetsAt}.` : ' It starts again on its own when the limit resets.')
842
+ : cause === 'too-old'
843
+ ? ` To update it, ${UPDATE}, and it starts again on its own.`
844
+ : cause === 'api-key'
845
+ ? ' Run codex login in a terminal to sign in with your ChatGPT plan, and it picks straight up.'
846
+ : cause === 'codex-config'
847
+ ? ` Run codex in a terminal to see the line it points at and fix it; ${Name} tries again on its own.`
848
+ : cause === 'open-elsewhere'
849
+ ? ` Close it there and ${Name} tries again on its own.`
481
850
  : cause === 'interrupted'
482
- ? ` ${Name} runs it again on its own.`
483
- : ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`;
851
+ ? ` ${Name} runs it again on its own${when}.`
852
+ : cause === 'model-not-on-plan'
853
+ ? (planEnded
854
+ ? ` That account's ChatGPT subscription ended on ${dayWords(planEnded)}, and the free plan does not include ${model || 'this model'}. Renew it at chatgpt.com, or pick another model for this task.`
855
+ : ' Pick another model for this task.')
856
+ : cause === 'level-not-offered'
857
+ ? ' Pick another thinking level for this task and it will run.'
858
+ : cause === 'network'
859
+ ? ` It will keep failing until this is fixed: if your company uses a proxy or its own certificate, ${NETWORK_FIX} and reopen ${Name}.`
860
+ : worked
861
+ ? ` ${Name} tries it again on its own${when}.`
862
+ : cause === 'workspace'
863
+ ? ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`
864
+ : ` ${UNNAMED_CHECK[engineWord] ?? ''}${Name} tries again on its own${when}.`;
484
865
 
485
866
  return `${opener}${again}${next}`;
486
867
  }
487
868
 
869
+ // Whether `deadRunSentence` tells her the app runs the task again by itself.
870
+ // The fresh-work pass keeps a row in the queue on exactly these, so a row is
871
+ // never promised a retry and then read as an answer waiting on her.
872
+ export function deadRunPromisesARetry({ cause = 'unknown', gaveUp = false } = {}) {
873
+ if (gaveUp) return false;
874
+ return cause !== 'org-blocked' && cause !== 'level-not-offered' && cause !== 'model-not-on-plan';
875
+ }
876
+
488
877
  /* ------------- a run that finished and still delivered nothing ------------ */
489
878
  // WHAT GOES ON HER ROW WHEN THE RUN WAS FINE AND HER ANSWER STILL WENT NOWHERE.
490
879
  //
@@ -513,7 +902,43 @@ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', r
513
902
  export function undeliveredAnswerSentence({ engineWord = 'The agent', attempts = 1 } = {}) {
514
903
  const n = Math.max(1, Math.trunc(attempts ?? 1));
515
904
  const runs = n === 1 ? 'once and finished' : `${n} times and each run finished`;
516
- 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.`;
905
+ return `${UNDELIVERED} ${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.`;
906
+ }
907
+
908
+ const UNDELIVERED = 'Your reply did not reach this task.';
909
+
910
+ // A THREAD THE APP STOPPED STARTING (`_noteHowTheRunStarted` in
911
+ // main/supervisor.mjs). `tries` is how many times it has tried again on its
912
+ // own since, and `next` when it tries again, null once it has stopped trying.
913
+ const STOPPED_STARTING = `${Name} stopped starting this thread`;
914
+ const clock = (t) => new Date(t).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }).toLowerCase().replace(/\s/g, '');
915
+
916
+ export function stoppedStartingSentence({ starts = 6, minutes = 10, tries = 0, triedAt = null, said = null, next = null } = {}) {
917
+ const what = tries === 0
918
+ ? `It was started ${starts} times in ${minutes} minutes, and each run ended before its agent began.`
919
+ : tries === 1
920
+ ? `It tried again on its own${triedAt ? ` at ${clock(triedAt)}` : ''}, and that run ended before its agent began too.`
921
+ : `It tried again on its own ${tries === 2 ? 'twice' : `${tries} times`}, and each run ended before its agent began.`;
922
+ const last = said ? ` The last one said "${said.slice(0, 300)}".` : '';
923
+ const then = next ? ` It tries again on its own at ${clock(next)}. Reply here to try again now.` : ' Reply here to try again.';
924
+ return `${STOPPED_STARTING}. ${what}${last}${then}`;
925
+ }
926
+
927
+ // THE APP'S OWN WORD THAT A RUN GOT NOWHERE: a dead-run sentence, from a run
928
+ // that used a tool or one that did not, the reply that never landed, or a
929
+ // thread it stopped starting. On a thread carrying a reply, each is written
930
+ // only once nothing will carry the reply on its own, so that thread waits on a
931
+ // person (`gaveUpOnTheAnswer`, renderer/src/list-rules.ts).
932
+ const DEAD_RUN_HALVES = {
933
+ 'at-limit': ['is at its usage limit', 'so nothing has been done on this task.'],
934
+ };
935
+
936
+ export function saysARunGotNowhere(text) {
937
+ const s = String(text ?? '');
938
+ if (!s.trim()) return false;
939
+ if (deadRunCause(s)) return true;
940
+ if (Object.values(WORKED_OPENERS).some((opener) => s.includes(opener))) return true;
941
+ return s.startsWith(UNDELIVERED) || s.startsWith(STOPPED_STARTING);
517
942
  }
518
943
 
519
944
  // WHOSE SENTENCE IS ON THIS ROW, AND WHAT IT SAYS HAPPENED.
@@ -530,12 +955,20 @@ export function undeliveredAnswerSentence({ engineWord = 'The agent', attempts =
530
955
  // So a result is asked whose it is. Null means somebody wrote a real answer
531
956
  // there and the row is not stranded; a cause means the last word on that row is
532
957
  // still the app saying nothing ran.
958
+ //
959
+ // A NAMED LOGIN SITS INSIDE ONE OPENER ("is at its usage limit on
960
+ // alex@acme.com, so nothing has been done"), so that one is read back by its
961
+ // two halves. Without this a row stopped counting as one nothing had run on
962
+ // the moment it said which account was spent.
533
963
  export function deadRunCause(text) {
534
964
  const s = String(text ?? '');
535
965
  if (!s.trim()) return null;
536
966
  for (const [cause, opener] of Object.entries(DEAD_RUN_OPENERS)) {
537
967
  if (s.includes(opener)) return cause;
538
968
  }
969
+ for (const [cause, halves] of Object.entries(DEAD_RUN_HALVES)) {
970
+ if (halves.every((half) => s.includes(half))) return cause;
971
+ }
539
972
  return null;
540
973
  }
541
974
 
@@ -558,10 +991,17 @@ export function deadRunCause(text) {
558
991
  // where the shape has no room for a sentence, which is most of them.
559
992
  export function causeWord(cause) {
560
993
  if (cause === 'signed-out') return 'a signed-out login';
994
+ if (cause === 'no-plan') return 'no plan to run on';
561
995
  if (cause === 'org-blocked') return 'an organization that blocks Claude Code';
996
+ if (cause === 'subscription-ended') return 'an ended Claude subscription';
562
997
  if (cause === 'at-limit') return 'your Claude limit';
563
998
  if (cause === 'workspace') return 'a folder Claude will not run in';
564
999
  if (cause === 'interrupted') return 'runs getting cut off';
1000
+ if (cause === 'model-not-on-plan') return "a model the account's plan does not include";
1001
+ if (cause === 'level-not-offered') return 'a thinking level its model does not offer';
1002
+ if (cause === 'network') return 'a network that blocks secure connections';
1003
+ if (cause === 'too-old') return 'an out-of-date Claude Code';
1004
+ if (cause === 'open-elsewhere') return 'a conversation open in another Codex window';
565
1005
  return 'something we cannot name';
566
1006
  }
567
1007
 
@@ -571,10 +1011,17 @@ export function causeWord(cause) {
571
1011
  export function causeWhat(cause, count) {
572
1012
  const n = Math.max(0, Math.trunc(count ?? 0));
573
1013
  if (cause === 'signed-out') return `${n} stopped by a signed-out Claude login`;
1014
+ if (cause === 'no-plan') return `${n} waiting on a plan to run on`;
574
1015
  if (cause === 'org-blocked') return `${n} stopped by an organization that blocks Claude Code`;
1016
+ if (cause === 'subscription-ended') return `${n} stopped by an ended Claude subscription`;
575
1017
  if (cause === 'at-limit') return `${n} waiting on your Claude limit`;
576
1018
  if (cause === 'workspace') return `${n} in a folder Claude will not run in`;
577
1019
  if (cause === 'interrupted') return `${n} cut off mid-run`;
1020
+ if (cause === 'model-not-on-plan') return `${n} asking for a model no signed-in Codex plan includes`;
1021
+ if (cause === 'level-not-offered') return `${n} asking for a thinking level its model does not offer`;
1022
+ if (cause === 'network') return `${n} stopped by a network that blocks secure connections`;
1023
+ if (cause === 'too-old') return `${n} stopped by an out-of-date Claude Code`;
1024
+ if (cause === 'open-elsewhere') return `${n} open in another Codex window`;
578
1025
  return `${n} stopped for a reason we cannot name`;
579
1026
  }
580
1027
 
@@ -582,10 +1029,17 @@ export function causeWhat(cause, count) {
582
1029
  // comes from the limit line the CLI printed, never from our own clock.
583
1030
  export function causeNext(cause, resetsAt = null) {
584
1031
  if (cause === 'signed-out') return 'Type /login in a terminal and they pick straight up.';
1032
+ if (cause === 'no-plan') return `Set one up ${SET_UP_A_PLAN}.`;
585
1033
  if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
1034
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai and they pick straight up.';
586
1035
  if (cause === 'at-limit') return resetsAt ? `They start again on their own at ${resetsAt}.` : 'They start again on their own when it resets.';
587
1036
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
588
1037
  if (cause === 'interrupted') return `Nothing to do. ${Name} is trying them again.`;
1038
+ if (cause === 'model-not-on-plan') return 'Pick another model on each.';
1039
+ if (cause === 'level-not-offered') return 'Pick another thinking level on each.';
1040
+ if (cause === 'network') return troubleRemedy('network');
1041
+ if (cause === 'too-old') return troubleRemedy('too-old');
1042
+ if (cause === 'open-elsewhere') return 'Close them in the other Codex window.';
589
1043
  return `${Name} keeps trying on its own.`;
590
1044
  }
591
1045
 
@@ -614,7 +1068,7 @@ export function causeBreakdown(rows = [], resetsAt = null) {
614
1068
  what: causeWhat(cause, count),
615
1069
  next: causeNext(cause, cause === 'at-limit' ? resetsAt : null),
616
1070
  act: causeAct(cause),
617
- hers: needsHerHands(cause) || cause === 'workspace',
1071
+ hers: needsHerHands(cause) || cause === 'workspace' || cause === 'model-not-on-plan' || cause === 'level-not-offered' || cause === 'network' || cause === 'open-elsewhere',
618
1072
  }))
619
1073
  .sort((a, b) => (b.hers - a.hers) || (b.count - a.count) || a.cause.localeCompare(b.cause));
620
1074
  }