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,481 @@
1
+ // The dashboard: the company's real state as one folded picture.
2
+ //
3
+ // This module is the PURE core, zero-dep ESM so main (the ledger store, the
4
+ // agent tools) and the renderer (the page) share exactly one set of rules. No
5
+ // filesystem, no network, no React, no Date.now except as an injected default,
6
+ // so every rule below is unit-testable without a DOM or a disk.
7
+ //
8
+ // Two halves, and the split is the whole design:
9
+ //
10
+ // DERIVED in-flight work, artifacts, folders, analytics numbers, how many
11
+ // days in. Computed at read time from state that already exists.
12
+ // Typed status rots; earned status stays true.
13
+ //
14
+ // WRITTEN the judgments a derivation cannot make: the one-liner, where the
15
+ // company is on the path, the bottleneck, the next move, which
16
+ // metrics matter. Appended to a ledger as partial patches by any
17
+ // agent (or the founder), folded newest-wins per field.
18
+ //
19
+ // The sharpest constraint in the whole feature, worth restating where the code
20
+ // lives: any pre-launch score that can be improved WITHOUT moving toward launch
21
+ // will be improved without moving toward launch. That is why the pre-launch
22
+ // headline is "steps to launch", which counts down and is derived from the
23
+ // path, rather than anything a founder can raise by interviewing forever.
24
+
25
+ /* ------------------------------- the stages ------------------------------ */
26
+ // Two states of one page, never two products. Pre-launch adds the path and the
27
+ // proportion pips; launched retires them and real metrics take over.). A
28
+ // founder who flips their own stage early is not a failure mode worth
29
+ // machinery.
30
+ export const STAGES = ['prelaunch', 'launched'];
31
+
32
+ /* -------------------------------- the path ------------------------------- */
33
+ // The default road to launch. Six steps, plain words, ending at the door: the
34
+ // point is showing it to people you do not know. An agent may replace this
35
+ // wholesale for a company these steps do not fit (a physical product, an
36
+ // existing business adding a product), which is why it is a ledger field and
37
+ // not a hard-coded ladder.
38
+ //
39
+ // Every step's `note` is a fact or a condition, never a target and never a
40
+ // countdown: comparison is motivating, prescription is not.
41
+ export const DEFAULT_PATH = [
42
+ { name: 'Describe it', status: 'here', note: '' },
43
+ { name: 'Meet your customers', status: 'ahead', note: '' },
44
+ { name: 'Talk to them', status: 'ahead', note: '' },
45
+ { name: 'Build it', status: 'ahead', note: '' },
46
+ { name: 'Put it online, take payment', status: 'ahead', note: '' },
47
+ { name: "Show it to people you don't know", status: 'ahead', note: 'the whole point' },
48
+ ];
49
+
50
+ export const STEP_STATUSES = ['done', 'here', 'ahead'];
51
+
52
+ // The last step is the door: it renders as wanted rather than as locked (full
53
+ // strength label, a lamp ring waiting). Position, not a flag on the data, so a
54
+ // replaced path gets the same treatment for free.
55
+ export function isDoor(path, index) {
56
+ return index === (path?.length ?? 0) - 1;
57
+ }
58
+
59
+ // How many steps remain, the pre-launch headline. Counts every step that is not
60
+ // done, so standing still cannot improve it and finishing one always does.
61
+ export function stepsToLaunch(path) {
62
+ const steps = Array.isArray(path) ? path : [];
63
+ return steps.filter((s) => s?.status !== 'done').length;
64
+ }
65
+
66
+ /* ------------------------------- the metrics ----------------------------- */
67
+ // A metric is one of three things, and the difference between the second and
68
+ // the third is a load-bearing honesty rule:
69
+ //
70
+ // a VALUE a real number we measured. `0` is a real number.
71
+ // ABSENT we are not collecting this. Says so, and says what would make
72
+ // it start. Renders over a dashed baseline.
73
+ // a PROPORTION "4 of 6", pre-launch, drawn as pips rather than a time series
74
+ // because the answer is out of six, not over days.
75
+ //
76
+ // Absence of data and a real zero are DIFFERENT FACTS and must never look the
77
+ // same. A page that draws "not measured" as a confident 0 is lying, and this
78
+ // product's whole claim is that its numbers are not lying.
79
+ export function makeMetric(input = {}) {
80
+ const value = Number.isFinite(input.value) ? input.value : null;
81
+ const of = Number.isFinite(input.of) ? input.of : null;
82
+ return {
83
+ id: String(input.id ?? '').trim() || slug(input.label),
84
+ label: String(input.label ?? '').trim(),
85
+ value,
86
+ of,
87
+ // A currency mark, and the only reason it exists: a big bare "1,240" reads
88
+ // as a count of things, not as money. Kept to a mark rather than a format
89
+ // so the number itself stays the number.
90
+ prefix: String(input.prefix ?? '').trim().slice(0, 4),
91
+ // the italic line beneath the number: what the number MEANS, in plain words
92
+ unit: String(input.unit ?? '').trim(),
93
+ // when there is no value: the short honest phrase ("not measured", "not
94
+ // yet") and the line that says what would fill it. Never a loading state.
95
+ absent: value == null ? (String(input.absent ?? '').trim() || 'not measured') : null,
96
+ fills: value == null ? String(input.fills ?? '').trim() : '',
97
+ series: Array.isArray(input.series) ? input.series.filter((n) => Number.isFinite(n)) : null,
98
+ source: input.source ?? null,
99
+ };
100
+ }
101
+
102
+ // Which drawing a metric gets. A proportion is pips; anything else with history
103
+ // is a sparkline; anything else is the dashed absent baseline. Pure, so the
104
+ // component never decides this itself and the tests can pin it.
105
+ export function metricShape(metric) {
106
+ if (!metric) return 'absent';
107
+ if (metric.of != null) return 'pips';
108
+ if (metric.value == null) return 'absent';
109
+ // A series that is zero every day is drawn as a real LINE on a real baseline,
110
+ // not as a row of bars with no height. A flat line reads as a measurement;
111
+ // the dashed baseline reads as an absence.
112
+ if (metric.series && metric.series.length) {
113
+ return metric.series.some((n) => n > 0) ? 'series' : 'flat';
114
+ }
115
+ return 'flat';
116
+ }
117
+
118
+ // True when this metric is honestly measured. `0` is measured; null is not.
119
+ export function isMeasured(metric) {
120
+ return !!metric && metric.value != null;
121
+ }
122
+
123
+ // `drillEvent` lived here: it answered which event a number could be opened
124
+ // into, and it did that by asking whether the metric's source kind was the
125
+ // other product's name. Removed on 2026-09-22 with the rest of that name. It
126
+ // had no callers in main, shared, renderer, tests or scripts, and nothing in
127
+ // this file ever wrote that kind (the only one written is 'derived'), so it
128
+ // returned null for every input it could ever have been given.
129
+
130
+ /* ------------------------------- the ledger ------------------------------ */
131
+ // Append-only, one JSON object per line:
132
+ // { ts, source: 'agent' | 'founder', chatId?, patch: { <field>: value } }
133
+ //
134
+ // Several conversations run at once and all of them may want to write, which is
135
+ // exactly why this is append-only: two chats appending concurrently cannot
136
+ // clobber each other the way two read-modify-writes of a single document would.
137
+ // Folding is newest-wins per TOP-LEVEL field, so a chat that only knows the
138
+ // next move never has to restate the one-liner to avoid erasing it.
139
+ export const LEDGER_FIELDS = [
140
+ 'founderName', 'oneLiner', 'stage', 'path', 'bottleneck', 'nextMove',
141
+ 'metrics', 'keyMetricId', 'analytics',
142
+ ];
143
+
144
+ export const LEDGER_SOURCES = ['agent', 'founder'];
145
+
146
+ // THE FOUNDER'S WORD WINS.). Authority is per FIELD and only over fields the
147
+ // founder actually touched, so pinning the headline metric never freezes the
148
+ // agent out of updating the metric values themselves. A newer founder patch
149
+ // always beats an older founder patch: changing your mind is allowed.
150
+ export function foldLedger(lines, now = Date.now()) {
151
+ const picture = defaultDashboard(now);
152
+ const winner = new Map(); // field -> { ts, source }
153
+
154
+ for (const line of Array.isArray(lines) ? lines : []) {
155
+ const entry = normalizeLine(line);
156
+ if (!entry) continue; // a malformed line is skipped, never fatal
157
+ for (const [field, value] of Object.entries(entry.patch)) {
158
+ if (!LEDGER_FIELDS.includes(field)) continue;
159
+ const held = winner.get(field);
160
+ if (held && !beats(entry, held)) continue;
161
+ const clean = coerceField(field, value);
162
+ if (clean === undefined) continue; // a field that failed validation keeps the older value
163
+ picture[field] = clean;
164
+ winner.set(field, { ts: entry.ts, source: entry.source });
165
+ }
166
+ }
167
+
168
+ // who last spoke for each field, so the page can show the founder that their
169
+ // own choice is the one in force rather than silently agreeing with them
170
+ picture.authored = Object.fromEntries([...winner].map(([f, w]) => [f, w.source]));
171
+ return picture;
172
+ }
173
+
174
+ // Does the incoming entry outrank the one already holding this field?
175
+ function beats(incoming, held) {
176
+ if (held.source === 'founder' && incoming.source !== 'founder') return false;
177
+ if (incoming.source === 'founder' && held.source !== 'founder') return true;
178
+ return incoming.ts >= held.ts;
179
+ }
180
+
181
+ // One ledger line, defensively read. Anything unparseable returns null so a
182
+ // corrupt ledger degrades to an older picture rather than a broken page.
183
+ export function normalizeLine(line) {
184
+ let raw = line;
185
+ if (typeof raw === 'string') {
186
+ const text = raw.trim();
187
+ if (!text) return null;
188
+ try { raw = JSON.parse(text); } catch { return null; }
189
+ }
190
+ if (!raw || typeof raw !== 'object') return null;
191
+ const patch = raw.patch;
192
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return null;
193
+ const ts = Number.isFinite(raw.ts) ? raw.ts : 0;
194
+ const source = LEDGER_SOURCES.includes(raw.source) ? raw.source : 'agent';
195
+ return { ts, source, chatId: raw.chatId ?? null, patch };
196
+ }
197
+
198
+ // Per-field validation. Returning undefined means "this patch had nothing
199
+ // usable for this field", and the previous value stands: a garbled write can
200
+ // never blank a good picture.
201
+ function coerceField(field, value) {
202
+ switch (field) {
203
+ case 'founderName':
204
+ // What to call them, as THEY said it. Never derived from an email local
205
+ // part: that produced "Morning, Sfeder." on the founder's own screen
206
+ // (2026-07-27) from a git-style handle, and a name is exactly the thing
207
+ // you do not guess. Absent, the page greets without a name, which is
208
+ // warm and correct; wrong, it is neither.
209
+ return typeof value === 'string' && value.trim() ? clean(value).slice(0, 40) : undefined;
210
+
211
+ case 'oneLiner':
212
+ return typeof value === 'string' && value.trim() ? clean(value) : undefined;
213
+
214
+ case 'stage':
215
+ return STAGES.includes(value) ? value : undefined;
216
+
217
+ case 'keyMetricId':
218
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
219
+
220
+ case 'path': {
221
+ if (!Array.isArray(value) || !value.length) return undefined;
222
+ const steps = value
223
+ .filter((s) => s && typeof s === 'object' && String(s.name ?? '').trim())
224
+ .map((s) => ({
225
+ name: clean(s.name),
226
+ status: STEP_STATUSES.includes(s.status) ? s.status : 'ahead',
227
+ note: clean(s.note ?? ''),
228
+ }));
229
+ return steps.length ? steps : undefined;
230
+ }
231
+
232
+ case 'bottleneck':
233
+ case 'nextMove': {
234
+ if (value === null) return null; // an explicit clear is legitimate
235
+ if (!value || typeof value !== 'object') return undefined;
236
+ const body = clean(value.body ?? '');
237
+ if (!body) return undefined;
238
+ return { body, foot: clean(value.foot ?? '') };
239
+ }
240
+
241
+ case 'metrics': {
242
+ if (!Array.isArray(value)) return undefined;
243
+ const metrics = value
244
+ .filter((m) => m && typeof m === 'object' && String(m.label ?? '').trim())
245
+ .map((m) => makeMetric({ ...m, label: clean(m.label), unit: clean(m.unit ?? ''), fills: clean(m.fills ?? ''), absent: clean(m.absent ?? '') }));
246
+ return metrics.length ? metrics : undefined;
247
+ }
248
+
249
+ case 'analytics': {
250
+ if (value === null) return null;
251
+ if (!value || typeof value !== 'object') return undefined;
252
+ const provider = String(value.provider ?? '').trim();
253
+ if (!provider) return undefined;
254
+ return {
255
+ provider,
256
+ projectId: value.projectId != null ? String(value.projectId) : null,
257
+ host: typeof value.host === 'string' && value.host.trim() ? value.host.trim() : null,
258
+ };
259
+ }
260
+
261
+ default:
262
+ return undefined;
263
+ }
264
+ }
265
+
266
+ // The picture before anyone has written anything: a brand new company on step
267
+ // one.
268
+ export function defaultDashboard(now = Date.now()) {
269
+ return {
270
+ stage: 'prelaunch',
271
+ founderName: null,
272
+ oneLiner: null,
273
+ path: DEFAULT_PATH.map((s) => ({ ...s })),
274
+ bottleneck: null,
275
+ nextMove: null,
276
+ metrics: [],
277
+ keyMetricId: null,
278
+ analytics: null,
279
+ authored: {},
280
+ now,
281
+ };
282
+ }
283
+
284
+ /* ------------------------- the metric set on screen ---------------------- */
285
+ // One large number on the left, three to its right. Which one is large is the
286
+ // founder's if they said so, otherwise the first the agent listed, otherwise
287
+ // the derived pre-launch headline.
288
+ //
289
+ // Pre-launch ALWAYS has "steps to launch" available as the headline even when
290
+ // no agent has written a single metric, because the page has to be honest and
291
+ // useful on day one, and that number is derived from the path so it cannot be
292
+ // gamed.
293
+ export function metricRow(picture, { others = 3 } = {}) {
294
+ // NO PICTURE IS NOT A COMPANY. Day one is `defaultDashboard`, which has the
295
+ // six-step path and therefore a true headline; a null picture means the
296
+ // ledger has not been read yet (on the web the brain is a sandbox that takes
297
+ // 30-40 seconds to wake, and the workspace paints from cache meanwhile).
298
+ // Deriving the headline from an absent path made those seconds print a
299
+ // confident "0" over "nothing left before launch", which told the founder her
300
+ // company had launched (report 2026-08-06). Invent nothing; the page draws
301
+ // its loading state from this emptiness.
302
+ if (!picture) return { key: null, others: [] };
303
+ const metrics = [...(picture.metrics ?? [])];
304
+ const prelaunch = picture?.stage !== 'launched';
305
+
306
+ if (prelaunch && !metrics.some((m) => m.id === 'steps-to-launch')) {
307
+ metrics.unshift(stepsToLaunchMetric(picture?.path ?? []));
308
+ }
309
+
310
+ let keyIndex = 0;
311
+ if (picture?.keyMetricId) {
312
+ const found = metrics.findIndex((m) => m.id === picture.keyMetricId);
313
+ if (found >= 0) keyIndex = found;
314
+ }
315
+
316
+ const key = metrics[keyIndex] ?? null;
317
+ const rest = metrics.filter((_, i) => i !== keyIndex).slice(0, others);
318
+ return { key, others: rest };
319
+ }
320
+
321
+ // The derived pre-launch headline. Its unit line NAMES the remaining steps, so
322
+ // the number is never a bare score: it says what it would take to move it.
323
+ export function stepsToLaunchMetric(path) {
324
+ const steps = Array.isArray(path) ? path : [];
325
+ const remaining = steps.filter((s) => s?.status !== 'done');
326
+ const names = remaining.map((s) => lowerFirst(s.name));
327
+ const unit = names.length === 0
328
+ ? 'nothing left before launch.'
329
+ : names.length === 1
330
+ ? `${names[0]}.`
331
+ : `${names.slice(0, -1).join(', ')}, then ${names[names.length - 1]}.`;
332
+ return makeMetric({
333
+ id: 'steps-to-launch',
334
+ label: 'Steps to launch',
335
+ value: remaining.length,
336
+ unit,
337
+ source: { kind: 'derived', of: 'path' },
338
+ });
339
+ }
340
+
341
+ // The pips under a pre-launch figure: a proportion, not a series. The path's
342
+ // own pips mark where you are with the lamp; a plain proportion has no "now".
343
+ export function pipsFor(metric, path) {
344
+ if (metric?.id === 'steps-to-launch') {
345
+ const steps = Array.isArray(path) ? path : [];
346
+ return steps.map((s, i) => {
347
+ if (s?.status === 'done') return 'on';
348
+ if (s?.status === 'here') return 'now';
349
+ // the first not-done step counts as where you are when nothing says 'here'
350
+ const firstOpen = steps.findIndex((x) => x?.status !== 'done');
351
+ return i === firstOpen && !steps.some((x) => x?.status === 'here') ? 'now' : 'off';
352
+ });
353
+ }
354
+ if (metric?.of == null) return [];
355
+ const total = Math.max(0, Math.round(metric.of));
356
+ const filled = Math.max(0, Math.min(total, Math.round(metric.value ?? 0)));
357
+ return Array.from({ length: total }, (_, i) => (i < filled ? 'on' : 'off'));
358
+ }
359
+
360
+ /* ------------------------------ in flight -------------------------------- */
361
+ // Four glyphs and no more, because the founder-facing question is only ever one
362
+ // of four: does it need me, is it moving, did it stall, is it ready.
363
+ //
364
+ // wait a conversation is holding a question for you
365
+ // run a turn is working right now, or a dev server is up
366
+ // stop something was running and is not any more
367
+ // done finished and you have not looked at it yet
368
+ //
369
+ // Everything here is DERIVED from state the app already keeps and never showed
370
+ // on this page: the per-chat status ledger and the runtime map. The old home's
371
+ // "in progress" lamp was not liveness at all and does not survive into this
372
+ // grid.
373
+ export const GLYPHS = ['wait', 'run', 'stop', 'done'];
374
+
375
+ // Order on screen: what needs you, then what is moving, then what stopped, then
376
+ // what is ready. Never a drought counter, never a scolding; a stalled item says
377
+ // when it stopped and nothing more.
378
+ const GLYPH_RANK = { wait: 0, run: 1, stop: 2, done: 3 };
379
+
380
+ export function deriveInFlight({ chats = [], chatStatus = {}, runtime = {}, creations = [] } = {}, now = Date.now()) {
381
+ const items = [];
382
+
383
+ for (const chat of chats) {
384
+ const st = chatStatus?.[chat?.id];
385
+ if (!st) continue;
386
+ if (st.askPending) {
387
+ items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'wait', state: 'waiting on you', detail: '', since: st.askSince ?? null });
388
+ } else if (st.working) {
389
+ items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'run', state: 'working', detail: elapsed(st.workingSince, now), since: st.workingSince ?? null });
390
+ } else if (st.doneAt && !st.readAt) {
391
+ items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'done', state: 'ready', detail: when(st.doneAt, now), since: st.doneAt });
392
+ } else if (st.cutAt) {
393
+ // interrupted by a sleep or a restart and never picked back up
394
+ items.push({ key: `chat:${chat.id}`, chatId: chat.id, name: chat.title || 'Untitled', glyph: 'stop', state: 'stopped', detail: when(st.cutAt, now), since: st.cutAt });
395
+ }
396
+ }
397
+
398
+ const titleOf = (id) => creations.find((c) => c?.id === id)?.title;
399
+ for (const [creationId, rt] of Object.entries(runtime ?? {})) {
400
+ const name = titleOf(creationId);
401
+ if (!name) continue; // a runtime whose creation is gone is not a thing to show
402
+ if (rt?.status === 'running' || rt?.status === 'starting') {
403
+ items.push({ key: `run:${creationId}`, creationId, name, glyph: 'run', state: rt.status === 'starting' ? 'starting' : 'running', detail: rt.port ? `port ${rt.port}` : '', since: null });
404
+ } else if (rt?.status === 'error') {
405
+ items.push({ key: `run:${creationId}`, creationId, name, glyph: 'stop', state: 'stopped', detail: 'it errored', since: null });
406
+ }
407
+ }
408
+
409
+ items.sort((a, b) => (GLYPH_RANK[a.glyph] - GLYPH_RANK[b.glyph]) || (b.since ?? 0) - (a.since ?? 0));
410
+ return items;
411
+ }
412
+
413
+ /* --------------------------------- time ---------------------------------- */
414
+ // Elapsed, for something happening right now. Minutes until an hour, then
415
+ // hours: a working turn measured in seconds reads as a stopwatch, which invites
416
+ // watching it rather than doing something else.
417
+ export function elapsed(since, now = Date.now()) {
418
+ if (!Number.isFinite(since) || since <= 0) return '';
419
+ const mins = Math.floor((now - since) / 60_000);
420
+ if (mins < 1) return 'just now';
421
+ if (mins < 60) return `${mins} min`;
422
+ const hours = Math.floor(mins / 60);
423
+ return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
424
+ }
425
+
426
+ // When something happened, in the same words the rest of the app uses.
427
+ export function when(ts, now = Date.now()) {
428
+ if (!Number.isFinite(ts) || ts <= 0) return '';
429
+ const days = Math.floor((now - ts) / 86_400_000);
430
+ if (days <= 0) return 'today';
431
+ if (days === 1) return 'yesterday';
432
+ if (days < 7) return `${days} days ago`;
433
+ return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
434
+ }
435
+
436
+ // The date on the top bar, in the app's house form: "Mon · Jul 27".
437
+ export function dateLine(now = Date.now()) {
438
+ const d = new Date(now);
439
+ const day = d.toLocaleDateString('en-US', { weekday: 'short' });
440
+ const rest = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
441
+ return `${day} · ${rest}`;
442
+ }
443
+
444
+ // NO PACE LINE, deliberately. The pre-launch design carried "day 4 · most
445
+ // launch by 9"; the founder cut it (2026-07-26) as incidental copy. Removing
446
+ // it also removes the only number on the page with nothing behind it: we have
447
+ // no launch cohort yet, so the comparison would have been invented, and a "day
448
+ // N" count standing alone is the drought counter the house rules already ban.
449
+ // If a real cohort ever exists, comparison is welcome back; a target is not.
450
+
451
+ /* ------------------------------- the greeting ---------------------------- */
452
+ // Time of day, for "Morning, the founder." Local to whoever is reading
453
+ // it.
454
+ export function greeting(now = Date.now()) {
455
+ const hour = new Date(now).getHours();
456
+ if (hour < 12) return 'Morning';
457
+ if (hour < 18) return 'Afternoon';
458
+ return 'Evening';
459
+ }
460
+
461
+ /* -------------------------------- helpers -------------------------------- */
462
+ // House rule, enforced rather than remembered: no em or en dashes in anything a
463
+ // founder reads. An agent writing the ledger is a model, and models reach for
464
+ // them constantly, so the ledger cleans on the way in instead of hoping.
465
+ export function clean(text) {
466
+ return String(text ?? '')
467
+ .replace(/\s*[—–]\s*/g, ', ')
468
+ .replace(/\s+/g, ' ')
469
+ .trim();
470
+ }
471
+
472
+ function slug(text) {
473
+ return String(text ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'metric';
474
+ }
475
+
476
+ function lowerFirst(text) {
477
+ const s = String(text ?? '').trim();
478
+ if (!s) return s;
479
+ // only lowercase a plain capitalized word, so a proper noun keeps its case
480
+ return /^[A-Z][a-z]/.test(s) ? s[0].toLowerCase() + s.slice(1) : s;
481
+ }
@@ -0,0 +1,127 @@
1
+ // WHAT A SESSION'S OWN TRACE LOG SAYS ABOUT WHETHER IT EVER GOT GOING.
2
+ //
3
+ // The supervisor already knows this while a session is alive: it holds the
4
+ // object and can see whether an id ever arrived. The trace is the only place
5
+ // that fact survives the process, and surviving the process is the whole point
6
+ // here.
7
+ //
8
+ // She was right, and the reason is a gap rather than a bug. The sentence that
9
+ // says a run died was written at the MOMENT of the exit, by the handler
10
+ // holding the session. Her four remaining Codex rows had already died before
11
+ // that code existed — the last of them at 13:48 and 13:51 — and nothing in the
12
+ // app ever looks at a row again to notice it is sitting there silent. So they
13
+ // read exactly as they read on Tuesday: in progress, nothing running, no word.
14
+ //
15
+ // A trace looks like this. The header, then whatever came out, then the exit:
16
+ //
17
+ // # There's something wrong with our search field. # · spawned
18
+ // 2026-08-27T20:51:39.445Z
19
+ //
20
+ // stderr: Reading additional input from stdin...
21
+ // Not inside a trusted directory and --skip-git-repo-check was not specified.
22
+ //
23
+ // # exited (1) 2026-08-27T20:51:40.139Z
24
+ //
25
+ // A run that actually started looks nothing like it: from the first line the
26
+ // CLI streams, the supervisor writes a clock-stamped line for every step. That
27
+ // stamp is the tell, and it is a better one than the exit code, because a
28
+ // session can run for six minutes of real work and still exit non-zero.
29
+
30
+ // Every narration line the supervisor writes is `HH:MM:SS something`. The
31
+ // header lines start with '#', the tool's own output is prefixed 'stderr: ',
32
+ // and neither can be mistaken for it.
33
+ const NARRATED = /^\d{2}:\d{2}:\d{2}\s{2}\S/m;
34
+
35
+ const EXITED = /^# exited \((-?\d+)\)/m;
36
+
37
+ // THE DIAGNOSIS IS ON THE SECOND LINE, NOT THE FIRST, and reading only the
38
+ // prefixed line loses it entirely. What the tool prints arrives as one chunk
39
+ // and is written with one `stderr: ` in front of the whole chunk, so a refusal
40
+ // that comes after a newline sits in the file with no prefix at all:
41
+ //
42
+ // stderr: Reading additional input from stdin...
43
+ // Not inside a trusted directory and --skip-git-repo-check was not specified.
44
+ //
45
+ // Taking prefixed lines alone therefore came back with "Reading additional
46
+ // input from stdin", which classifies as 'unknown', and the row would have
47
+ // been told the generic sentence about her four Codex tasks whose cause we
48
+ // knew exactly. So a chunk runs from its `stderr:` line until the blank line
49
+ // that ends it, and nothing of ours can be swept up on the way: the header and
50
+ // the narration both end the chunk too.
51
+ function stderrChunks(raw) {
52
+ const out = [];
53
+ let inside = false;
54
+ for (const line of raw.split('\n')) {
55
+ if (line.startsWith('stderr:')) {
56
+ inside = true;
57
+ out.push(line.replace(/^stderr:\s?/, ''));
58
+ continue;
59
+ }
60
+ if (!inside) continue;
61
+ if (!line.trim() || line.startsWith('#') || /^\d{2}:\d{2}:\d{2}\s{2}/.test(line)) { inside = false; continue; }
62
+ out.push(line);
63
+ }
64
+ return out.join('\n').slice(-1000);
65
+ }
66
+
67
+ // AND THE OTHER PLACE A REASON HIDES, which is not stderr at all.
68
+ //
69
+ // A CLI that starts, authenticates and fails says so through its own protocol,
70
+ // and the supervisor writes that as the run's result with an ERROR flag on it
71
+ // (`== RESULT (success ERROR · 1 turns) ==`). Nothing about it is on stderr.
72
+ // Measured on her store 2026-08-27: had 238 traces and 197 recorded strikes,
73
+ // every one of them
74
+ //
75
+ // Failed to authenticate: OAuth session expired and could not be refreshed
76
+ //
77
+ // and the row had never carried a single word. Read from stderr alone the
78
+ // cause came back 'unknown' and she would have been told the row "stopped
79
+ // before it did anything", which is true and useless: the one sentence worth
80
+ // having here is the one that tells her to type /login.
81
+ //
82
+ // Only ever read when the flag is on the line. A result WITHOUT the ERROR flag
83
+ // is a worker's own prose about her product, and prose is exactly the thing
84
+ // that must not be pattern-matched for words like "unauthorized".
85
+ const ERROR_RESULT = /^\d{2}:\d{2}:\d{2}\s{2}== RESULT \([^)]*\bERROR\b[^)]*\) ==$/m;
86
+
87
+ function errorResult(raw) {
88
+ const at = ERROR_RESULT.exec(raw);
89
+ if (!at) return '';
90
+ const after = raw.slice(at.index + at[0].length);
91
+ const end = after.search(/^# exited \(/m);
92
+ return (end === -1 ? after : after.slice(0, end)).trim();
93
+ }
94
+
95
+ /**
96
+ * @param {string} text the whole trace file
97
+ * @returns {{everRan: boolean, exitCode: number|null, stderr: string}}
98
+ * `stderr` is what the tool said about itself, from either place it says it.
99
+ * `everRan` false means the CLI never produced a first line: the process
100
+ * started and died before it was a session. `exitCode` null means the trace
101
+ * has no exit line at all, which is what a run killed with the app looks
102
+ * like, and is deliberately NOT read as a death.
103
+ */
104
+ export function readDeadRunTrace(text) {
105
+ const raw = String(text ?? '');
106
+ const exited = EXITED.exec(raw);
107
+ return {
108
+ everRan: NARRATED.test(raw),
109
+ exitCode: exited ? Number(exited[1]) : null,
110
+ stderr: [stderrChunks(raw), errorResult(raw)].filter(Boolean).join('\n').slice(-1000),
111
+ };
112
+ }
113
+
114
+ /**
115
+ * IS THIS THE TRACE OF A RUN THAT LEFT HER NOTHING?
116
+ *
117
+ * Both halves have to be true. It has to have finished — a trace with no exit
118
+ * line belongs to a session that is either still going or was killed with the
119
+ * app, and neither is news. And it has to have finished BADLY: either it never
120
+ * narrated a line, so it was never a session at all, or it exited non-zero.
121
+ * A clean exit means a worker ran and made its own choices about the row, and
122
+ * the supervisor has no business speaking over it.
123
+ */
124
+ export function traceShowsASilentDeath(trace) {
125
+ if (!trace || trace.exitCode == null) return false;
126
+ return !trace.everRan || trace.exitCode !== 0;
127
+ }