agentbox-app 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -0,0 +1,114 @@
1
+ // A PICTURE'S WIDTH AND HEIGHT, OFF THE FIRST BYTES OF ITS FILE.
2
+ //
3
+ // The phone draws a picture it has never seen as a 4:3 box until the last byte
4
+ // arrives, because WebKit gives a picture no size before it is whole, and then
5
+ // the words under it jump by the difference: 514px for a phone screenshot at
6
+ // 390px wide (tests/a-picture-the-phone-has-never-seen-gets-its-shape-before-it-arrives).
7
+ // The phone asks for this first (main/serve.mjs, /f/<key>/shape/) and draws
8
+ // the box at the picture's own shape.
9
+ //
10
+ // PNG, GIF, JPEG and WebP, which is what agents and phones make. Anything else,
11
+ // or a file cut short, is null: the box stays 4:3, which is no worse than it
12
+ // was, rather than a guessed shape that is wrong.
13
+ import fs from 'node:fs';
14
+
15
+ /** How much of a file is read. A JPEG's frame sits after its Exif and colour profile. */
16
+ const HEAD_BYTES = 256 * 1024;
17
+
18
+ const shape = (w, h) => (w > 0 && h > 0 ? { w, h } : null);
19
+
20
+ function png(b) {
21
+ if (b.length < 24 || b.readUInt32BE(0) !== 0x89504e47 || b.toString('latin1', 12, 16) !== 'IHDR') return null;
22
+ return shape(b.readUInt32BE(16), b.readUInt32BE(20));
23
+ }
24
+
25
+ function gif(b) {
26
+ if (b.length < 10 || b.toString('latin1', 0, 4) !== 'GIF8') return null;
27
+ return shape(b.readUInt16LE(6), b.readUInt16LE(8));
28
+ }
29
+
30
+ function webp(b) {
31
+ if (b.length < 30 || b.toString('latin1', 0, 4) !== 'RIFF' || b.toString('latin1', 8, 12) !== 'WEBP') return null;
32
+ const chunk = b.toString('latin1', 12, 16);
33
+ if (chunk === 'VP8X') return shape(1 + b.readUIntLE(24, 3), 1 + b.readUIntLE(27, 3));
34
+ if (chunk === 'VP8 ') return shape(b.readUInt16LE(26) & 0x3fff, b.readUInt16LE(28) & 0x3fff);
35
+ if (chunk === 'VP8L') {
36
+ const bits = b.readUInt32LE(21);
37
+ return shape(1 + (bits & 0x3fff), 1 + ((bits >>> 14) & 0x3fff));
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /** The Exif orientation in an APP1 block, or 0. 5 to 8 mean it is drawn turned a quarter. */
43
+ function orientation(b, start, end) {
44
+ if (b.toString('latin1', start, start + 6) !== 'Exif\0\0') return 0;
45
+ const tiff = start + 6;
46
+ if (tiff + 8 > end) return 0;
47
+ const little = b.toString('latin1', tiff, tiff + 2) === 'II';
48
+ const u16 = (at) => (little ? b.readUInt16LE(at) : b.readUInt16BE(at));
49
+ const u32 = (at) => (little ? b.readUInt32LE(at) : b.readUInt32BE(at));
50
+ const ifd = tiff + u32(tiff + 4);
51
+ if (ifd + 2 > end) return 0;
52
+ const count = u16(ifd);
53
+ for (let i = 0; i < count; i++) {
54
+ const entry = ifd + 2 + i * 12;
55
+ if (entry + 12 > end) return 0;
56
+ if (u16(entry) === 0x0112) return u16(entry + 8);
57
+ }
58
+ return 0;
59
+ }
60
+
61
+ function jpeg(b) {
62
+ if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
63
+ let turned = 0;
64
+ let i = 2;
65
+ while (i + 4 <= b.length) {
66
+ if (b[i] !== 0xff) return null;
67
+ const marker = b[i + 1];
68
+ // Fill bytes before a marker.
69
+ if (marker === 0xff) { i += 1; continue; }
70
+ const length = b.readUInt16BE(i + 2);
71
+ // Every start-of-frame but the three that are not frames (DHT, JPG, DAC).
72
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
73
+ if (i + 9 > b.length) return null;
74
+ const h = b.readUInt16BE(i + 5);
75
+ const w = b.readUInt16BE(i + 7);
76
+ return turned >= 5 && turned <= 8 ? shape(h, w) : shape(w, h);
77
+ }
78
+ if (marker === 0xe1 && !turned) turned = orientation(b, i + 4, Math.min(b.length, i + 2 + length));
79
+ i += 2 + length;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /** `{ w, h }` as the browser will draw it, or null. */
85
+ export function pictureShape(bytes) {
86
+ if (!bytes?.length) return null;
87
+ try {
88
+ return png(bytes) ?? gif(bytes) ?? webp(bytes) ?? jpeg(bytes);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /** The same, read off a file on disk without waiting, for a pass that lists many. */
95
+ export function readPictureShapeSync(file) {
96
+ const fd = fs.openSync(file, 'r');
97
+ try {
98
+ const buffer = Buffer.alloc(HEAD_BYTES);
99
+ return pictureShape(buffer.subarray(0, fs.readSync(fd, buffer, 0, HEAD_BYTES, 0)));
100
+ } finally {
101
+ fs.closeSync(fd);
102
+ }
103
+ }
104
+
105
+ /** The same, read off a file on disk. */
106
+ export async function readPictureShape(file) {
107
+ const handle = await fs.promises.open(file, 'r');
108
+ try {
109
+ const { buffer, bytesRead } = await handle.read({ buffer: Buffer.alloc(HEAD_BYTES), position: 0 });
110
+ return pictureShape(buffer.subarray(0, bytesRead));
111
+ } finally {
112
+ await handle.close();
113
+ }
114
+ }
@@ -0,0 +1,143 @@
1
+ // THE WAY OUT, AND NOTHING ON IT MAY KILL THE APP (w-60c8ddbab6, 2026-10-15).
2
+ //
3
+ // Quitting used to be six bare `app.on('before-quit', ...)` listeners and one
4
+ // `will-quit` that started the analytics flush and did not wait for it. Both
5
+ // halves of that cost us the end of every session.
6
+ //
7
+ // ONE STEP THAT THROWS ENDED THE WHOLE QUIT. On 0.1.13 the phone step did:
8
+ // closing the main window nulls `window`, and `phone.close()` makes the relay
9
+ // report the change back to a handler that asked the window whether it was
10
+ // destroyed (main/main.mjs, the `zero:phone-changed` line). An EventEmitter
11
+ // stops calling listeners at the first one that throws, `main/crash-report.mjs`
12
+ // rethrows an uncaught main error on purpose, and the process then dies
13
+ // fatally. Measured on an Electron harness: `updater.stop()` never ran,
14
+ // `will-quit` never fired, `analytics.flush()` never ran, and `quit` never
15
+ // came. The window's own guard is fixed where it lives, but the shape was the
16
+ // real fault: a quit built so that any one step can swallow the rest is a quit
17
+ // that will do this again for some other reason.
18
+ //
19
+ // AND THE FLUSH WAS FIRE AND FORGET. `will-quit` without `preventDefault` means
20
+ // the process goes as soon as the listeners return, so a batch handed to
21
+ // `posthog-node` a moment earlier is a POST that never leaves. Measured the
22
+ // same way: the flush resolved AFTER `process.on('exit')` had already run, and
23
+ // it only resolved at all because the harness's destination was in the same
24
+ // process. Against us.i.posthog.com it is a race the app loses.
25
+ //
26
+ // WHICH MATTERS BECAUSE THE END OF A SESSION IS WHAT WE READ RETENTION FROM.
27
+ // Of the fourteen installs that arrived on 0.1.13 or 0.1.14, thirteen opened
28
+ // the app once and never opened it again (PostHog, 2026-10-15), so for thirteen
29
+ // of fourteen people the batch dropped here is the LAST thing we would ever
30
+ // have heard, and the crash report explaining it is still sitting on their disk
31
+ // waiting for a launch that is not coming.
32
+
33
+ /** How long the quit is held open for the last batch of counts, and no longer. */
34
+ export const FLUSH_DEADLINE_MS = 2000;
35
+
36
+ /**
37
+ * How long a quit we asked for is given before the process is ended outright.
38
+ * Longer than the flush above on purpose: a quit holding itself open for the
39
+ * last counts is a quit that is working. */
40
+ export const EXIT_DEADLINE_MS = 8000;
41
+
42
+ /**
43
+ * How long the second ask gets, once the counts are out and there is nothing
44
+ * left to wait for. On Linux a quit asked for with a window still open — the
45
+ * menu's Quit, the tray's Quit — ignores it outright
46
+ * (tests/a-quit-held-for-the-last-counts-still-ends-the-app), and by then the
47
+ * window is gone and there is nothing left to show anybody. */
48
+ export const SECOND_QUIT_DEADLINE_MS = 2000;
49
+
50
+ /**
51
+ * A STEP ON THE WAY OUT, which runs once and cannot throw.
52
+ *
53
+ * `name` is in the warning rather than in a comment, because the one thing
54
+ * somebody reading a log needs to know is which step gave up. A swallow with
55
+ * nothing said is how this bug lived through three releases.
56
+ *
57
+ * ONCE, not once per `app.quit()`. Holding the quit for the flush below means
58
+ * calling `app.quit()` a second time, which emits `before-quit` again, and a
59
+ * `killAll` or a `close` run twice is at best wasted and at worst a second
60
+ * teardown of something already gone.
61
+ */
62
+ export function quitStep(app, event, name, fn, { log = console } = {}) {
63
+ let ran = false;
64
+ app.on(event, () => {
65
+ if (ran) return;
66
+ ran = true;
67
+ try {
68
+ fn();
69
+ } catch (err) {
70
+ // Never rethrown: the app is on its way out and every step after this one
71
+ // still has to happen.
72
+ log?.warn?.(`zero: ${name} failed while quitting: ${err?.message ?? err}`);
73
+ }
74
+ });
75
+ }
76
+
77
+ /**
78
+ * HOLD THE QUIT OPEN FOR THE LAST BATCH OF COUNTS.
79
+ *
80
+ * Three things it will not do, each of them a way a quit becomes a hang:
81
+ *
82
+ * - It will not wait past `FLUSH_DEADLINE_MS`. A machine with no network, or
83
+ * a destination that accepts the connection and never answers, must cost
84
+ * somebody two seconds on the way out and not their afternoon.
85
+ * - It will not care whether the flush worked. There is nothing to be done
86
+ * about a flush that failed, and a count is never worth a quit.
87
+ * - It will not hold twice. `will-quit` comes round again when we ask again,
88
+ * and holding that one too is a quit that never happens.
89
+ * - It will not trust the second ask either. On Linux, a quit that began with
90
+ * a window open ignores it and the process stays for ever, which is what
91
+ * `quitThenExit` is for
92
+ * (tests/a-quit-held-for-the-last-counts-still-ends-the-app).
93
+ */
94
+ export function flushBeforeQuit(app, flush, { deadlineMs = FLUSH_DEADLINE_MS, log = console } = {}) {
95
+ let held = false;
96
+ app.on('will-quit', (event) => {
97
+ if (held) return; // the second time round is the one we asked for
98
+ held = true;
99
+ event?.preventDefault?.();
100
+ let gone = false;
101
+ const go = () => {
102
+ if (gone) return;
103
+ gone = true;
104
+ clearTimeout(timer);
105
+ quitThenExit(app, { what: 'the last counts', deadlineMs: SECOND_QUIT_DEADLINE_MS, log });
106
+ };
107
+ const timer = setTimeout(go, deadlineMs);
108
+ // The deadline is armed before the flush is even called, so a `flush` that
109
+ // throws synchronously is covered by the same promise as one that rejects.
110
+ Promise.resolve()
111
+ .then(() => flush())
112
+ .catch((err) => { log?.warn?.(`zero: the last counts did not get out: ${err?.message ?? err}`); })
113
+ .then(go, go);
114
+ });
115
+ }
116
+
117
+ /**
118
+ * ASK TO QUIT, AND GO ANYWAY IF THE QUIT DOES NOT COME.
119
+ *
120
+ * `app.quit()` only asks, and on Linux an app that has already refused one
121
+ * `will-quit` ignores every ask after it: the measurement is in
122
+ * tests/a-quit-held-for-the-last-counts-still-ends-the-app. It is still asked,
123
+ * because that is what runs the quit steps, lets the workers go and fires
124
+ * `quit` for whoever is listening, and the deadline ends the process when the
125
+ * quit has not.
126
+ *
127
+ * Both callers pass a deadline that suits what they are waiting for: a
128
+ * screenshot run has a whole quit ahead of it, the flush above has nothing
129
+ * left but the `quit` event. The timer is unreffed: an app with nothing left
130
+ * to do must not be kept alive by the thing watching it. */
131
+ export function quitThenExit(app, { what = 'this run', deadlineMs = EXIT_DEADLINE_MS, log = console } = {}) {
132
+ let over = false;
133
+ const timer = setTimeout(() => {
134
+ if (over) return;
135
+ over = true;
136
+ log?.warn?.(`zero: ${what} asked the app to quit ${deadlineMs} ms ago and it is still here, so it is ending now`);
137
+ app.exit(0);
138
+ }, deadlineMs);
139
+ timer.unref?.();
140
+ app.once?.('quit', () => { over = true; clearTimeout(timer); });
141
+ app.quit();
142
+ return timer;
143
+ }
@@ -4,11 +4,24 @@ import {rememberCodexModels} from './codex-models.mjs';
4
4
  import {forget,claudeModels} from './claude-models.mjs';
5
5
  import {terminalEnv} from './task-terminals.mjs';
6
6
  import {codexLaunchEnv} from './codex-launch-env.mjs';
7
- export async function refreshAgentModels(engine,config,home){
7
+ import {ensureCodexHome} from './codex-account.mjs';
8
+ // `homes` is every Codex login to ask, one after another. A login that cannot
9
+ // answer keeps its last list and does not cost the others theirs; only all of
10
+ // them failing is an error.
11
+ export async function refreshAgentModels(engine,config,homes,{open=home=>openAppServer(config,home)}={}){
8
12
  if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
9
13
  // Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
10
14
  if(!config.codexBin)return;
11
- const client=createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
15
+ const list=[...new Set([homes].flat().filter(Boolean))];const errors=[];
16
+ for(const home of list){try{await refreshOne(open(home),home);}catch(error){errors.push(error);}}
17
+ if(list.length&&errors.length===list.length)throw errors[0];
18
+ }
19
+ function openAppServer(config,home){
20
+ // codex-cli refuses a CODEX_HOME that does not exist rather than making it.
21
+ ensureCodexHome(home);
22
+ return createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
23
+ }
24
+ async function refreshOne(client,home){
12
25
  const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
13
26
  try{
14
27
  await client.initialize();let cursor=null;const models=[];const seen=new Set();
@@ -26,6 +26,19 @@ const PASS = ['accept', 'content-type', 'range', 'user-agent', 'if-none-match',
26
26
  * 30 s the room gives a Mac to start answering (relay/room.mjs).
27
27
  */
28
28
  const WAIT_MS = 10_000;
29
+ /**
30
+ * THE LINK IS CHECKED, NOT TRUSTED (tests/the-computer-notices-a-dead-relay-link-and-reconnects).
31
+ * A socket that carries nothing cannot tell this end it has died: a laptop that
32
+ * slept or changed networks kept one it believed open while the relay had let
33
+ * it go, and phones were told the computer was not connected. So this end says
34
+ * "ping" this often and the relay answers "pong" without waking the room.
35
+ */
36
+ export const PING_MS = 25_000;
37
+ /** How long past a ping's turn a missing pong is allowed before the link is called dead. */
38
+ export const PONG_GRACE_MS = 10_000;
39
+ /** What the platform sends as text on the socket. Never a request: those are bytes. */
40
+ const PING = 'ping';
41
+ const PONG = 'pong';
29
42
  /** What the phone does not want back: the relay's stream is its own framing. */
30
43
  const DROP = new Set(['connection', 'keep-alive', 'transfer-encoding', 'content-length', 'content-encoding']);
31
44
 
@@ -34,9 +47,14 @@ const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0'))
34
47
 
35
48
  export function createRelayClient({
36
49
  relayUrl, key, port, WebSocketImpl = globalThis.WebSocket, fetchImpl = (...a) => globalThis.fetch(...a),
37
- log = console, onChange = () => {}, waitMs = WAIT_MS,
50
+ log = console, onChange = () => {}, waitMs = WAIT_MS, pingMs = PING_MS, graceMs = PONG_GRACE_MS,
38
51
  }) {
39
52
  let ws = null;
53
+ let pinger = null;
54
+ /** When the relay last answered a ping on this link; null until it ever has. */
55
+ let ponged = null;
56
+ /** When the ping timer last ran, to tell a sleep from a slow tick. */
57
+ let ticked = 0;
40
58
  let want = false;
41
59
  let connected = false;
42
60
  let error = null;
@@ -147,6 +165,10 @@ export function createRelayClient({
147
165
  }
148
166
 
149
167
  async function onMessage(sock, data) {
168
+ if (typeof data === 'string') {
169
+ if (data === PONG) ponged = Date.now();
170
+ return;
171
+ }
150
172
  const bytes = data instanceof ArrayBuffer ? new Uint8Array(data)
151
173
  : data instanceof Uint8Array ? data
152
174
  : new Uint8Array(await data.arrayBuffer());
@@ -178,6 +200,49 @@ export function createRelayClient({
178
200
  }
179
201
  }
180
202
 
203
+ // Two ways a link is known dead without waiting on the operating system: a
204
+ // relay that has answered pings stops answering, or this timer ran far too
205
+ // late, which is a laptop waking from sleep with every socket gone under it.
206
+ // A relay that never answers (one deployed before pings) is only ever judged
207
+ // by the second, so an old relay is not mistaken for a dead link.
208
+ function startPinging(sock) {
209
+ stopPinging();
210
+ ponged = null;
211
+ ticked = Date.now();
212
+ pinger = setInterval(() => {
213
+ const now = Date.now();
214
+ const slept = now - ticked > pingMs * 2;
215
+ ticked = now;
216
+ const silent = ponged !== null && now - ponged > pingMs + graceMs;
217
+ if (slept || silent) {
218
+ log.info?.(`[relay] the link went quiet (${slept ? 'woke from sleep' : 'no answer to ping'}); reconnecting`);
219
+ replace(sock);
220
+ return;
221
+ }
222
+ send(sock, PING);
223
+ }, pingMs);
224
+ }
225
+
226
+ function stopPinging() {
227
+ if (pinger) clearInterval(pinger);
228
+ pinger = null;
229
+ }
230
+
231
+ /** Let a dead link go and open a new one now, not after the backoff. */
232
+ function replace(sock) {
233
+ if (ws !== sock) return;
234
+ stopPinging();
235
+ ws = null;
236
+ connected = false;
237
+ for (const c of live.values()) c.abort();
238
+ live.clear();
239
+ try { sock.close(4002, 'No answer; reconnecting.'); } catch { /* already gone */ }
240
+ if (timer) { clearTimeout(timer); timer = null; }
241
+ retry = 1000;
242
+ onChange();
243
+ void connect();
244
+ }
245
+
181
246
  function schedule() {
182
247
  if (!want || timer) return;
183
248
  timer = setTimeout(() => { timer = null; void connect(); }, retry);
@@ -198,12 +263,14 @@ export function createRelayClient({
198
263
  connected = true;
199
264
  error = null;
200
265
  retry = 1000;
266
+ startPinging(sock);
201
267
  onChange();
202
268
  };
203
269
  sock.onmessage = (event) => { if (ws === sock) void onMessage(sock, event.data); };
204
270
  sock.onerror = () => { if (ws === sock && !connected) error = 'Could not reach the relay.'; };
205
271
  sock.onclose = (event) => {
206
272
  if (ws !== sock) return;
273
+ stopPinging();
207
274
  ws = null;
208
275
  const was = connected;
209
276
  connected = false;
@@ -226,6 +293,7 @@ export function createRelayClient({
226
293
  stop() {
227
294
  want = false;
228
295
  gen += 1;
296
+ stopPinging();
229
297
  if (timer) { clearTimeout(timer); timer = null; }
230
298
  const sock = ws;
231
299
  ws = null;
package/main/repeats.mjs CHANGED
@@ -16,6 +16,7 @@ import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import {
18
18
  DAY_NAMES, foldRepeats, isOwed, isRepeatRule, occurrenceId, periodKey, repeatLabel,
19
+ standingInstruction, PROTOCOL_OPENS, INSTRUCTION_MARK,
19
20
  } from '../shared/repeats.mjs';
20
21
  import { machineryPath } from './store/home.mjs';
21
22
  import { NAME } from '../shared/product-name.mjs';
@@ -41,8 +42,8 @@ const MISSES_BEFORE_ALERT = 3;
41
42
  // last can be truncated away entirely, and a worker that never saw it cannot
42
43
  // know that a quiet finish has to be marked.
43
44
  export const OCCURRENCE_PROTOCOL = [
44
- 'THIS IS ONE RUN OF A REPEATING TASK. It runs again tomorrow whatever happens',
45
- 'here, so this run does not have to carry the future.',
45
+ `${PROTOCOL_OPENS} It runs again at its next time whatever`,
46
+ 'happens here, so this run does not have to carry the future.',
46
47
  '',
47
48
  'FINISH ONE OF TWO WAYS.',
48
49
  '',
@@ -60,7 +61,12 @@ export const OCCURRENCE_PROTOCOL = [
60
61
  `only place in ${NAME} where finishing quietly hides something, so the marker is`,
61
62
  'required to hide, never to show.',
62
63
  '',
63
- 'HER INSTRUCTION FOLLOWS.',
64
+ 'IF THEY TELL YOU HOW THIS TASK SHOULD GO FROM NOW ON, CHANGE IT YOURSELF with',
65
+ 'the store tool set_repeating_task. Editing this run or writing a memory note',
66
+ 'reaches no later run: this body is a copy, and every run gets a new task',
67
+ 'folder. Never ask them to go and edit the task by hand.',
68
+ '',
69
+ INSTRUCTION_MARK,
64
70
  ].join('\n');
65
71
 
66
72
  export class Repeats {
@@ -212,7 +218,7 @@ export class Repeats {
212
218
 
213
219
  const { created } = workItemsDisk.createWorkItemIfAbsent(productDir, id, {
214
220
  title: `${rule.title} · ${dayStamp(key)}`,
215
- body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${rule.body ?? rule.title}`,
221
+ body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${standingInstruction(rule)}`,
216
222
  kind: 'directive',
217
223
  priority: rule.priority ?? 5,
218
224
  labels: ['founder', repeatLabel(ruleId)],
@@ -1,4 +1,5 @@
1
1
  import { harnessFor } from './harnesses.mjs';
2
+ import { teammateIdOf } from '../shared/teammates.mjs';
2
3
  // THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM THE FIRST SENTENCE.
3
4
  //
4
5
  // A dictated message keeps its first line as its title, verbatim and forever
@@ -62,51 +63,18 @@ export const NAME_MAX = 64;
62
63
  * archive; everything else is a row that can still appear in a list. */
63
64
  const LIVE = new Set(['open', 'claimed', 'blocked']);
64
65
 
65
- /**
66
- * PURE. Is this title already a name somebody wrote?
67
- *
68
- * The one question that decides whether this module speaks at all, and it is
69
- * answered off the words alone because that is all there is: nothing on a row
70
- * records whether its title was typed or derived.
71
- *
72
- * Three marks, and together they separate the two shapes `splitMessage` makes
73
- * (renderer/src/message-split.ts). A first line of 120 characters or less is
74
- * kept VERBATIM and is the name its person wrote. A longer one is clipped into
75
- * a title by the app, and every clip it can produce is either a whole sentence
76
- * of at least 25 characters or a hard cut ending in an ellipsis.
77
- *
78
- * as short as a name we would write NAME_MAX, so a title already that tight
79
- * has nothing for us to improve,
80
- * as few words as a name has the same eight `cleanName` allows,
81
- * not ending as a sentence does '.', '!', '?' and '…' are how a clipped
82
- * title ends and not how a name does.
83
- *
84
- * It errs towards leaving the words alone, which is the right way to be wrong:
85
- * the cost is a short typed sentence keeping its own first line, and the cost
86
- * the other way is somebody losing the thread they named.
87
- */
88
- export function isWrittenName(title) {
89
- const t = String(title ?? '').trim();
90
- if (!t) return false;
91
- if (t.length > NAME_MAX) return false;
92
- if (t.split(/\s+/).length > 8) return false;
93
- return !/[.!?…]$/.test(t);
94
- }
95
-
96
66
  /**
97
67
  * PURE. Does this row want naming?
98
68
  *
99
- * Four ways to say no, and the last two are the fix for a tester losing their
100
- * own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
101
- * header comment" and then "Update cart file comment" inside two minutes.
69
+ * Three ways to say no:
102
70
  *
103
71
  * - it is done, so nobody is scanning for it,
104
72
  * - it has nothing to name from, which is a row with no words on it yet,
105
- * - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
106
- * look for, and on a team it is the name they will point someone else at.
107
- * Improving it is not an improvement.
108
73
  * - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
109
74
  *
75
+ * A short typed title is named too: a list where some rows are typed lines and
76
+ * the rest are names reads as two styles. The prompt keeps such a line's words.
77
+ *
110
78
  * That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
111
79
  * followed where the thread stood: an agent writes a note and a result on every
112
80
  * run, so every run earned a new name. Following the thread was the intended
@@ -122,8 +90,8 @@ export function isWrittenName(title) {
122
90
  */
123
91
  export function wantsName(item) {
124
92
  if (!item || !LIVE.has(item.status)) return false;
93
+ if (teammateIdOf(item)) return false;
125
94
  if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
126
- if (isWrittenName(item.title)) return false;
127
95
  return !String(item.label ?? '').trim();
128
96
  }
129
97
 
@@ -139,12 +107,15 @@ export function namePrompt(item) {
139
107
  'Here is a work thread: the message that opened it, and the latest thing',
140
108
  'written on it. Write a short name for the thread AS IT STANDS NOW: 3 to 7',
141
109
  'words, sentence case, no quotes, no trailing punctuation, naming the thing',
142
- 'being worked on rather than restating the sentence. Reply with the name and',
143
- 'nothing else.',
110
+ 'being worked on rather than restating the sentence. If the opening line is',
111
+ 'already a short name, keep its words and only fix the case. Reply with the',
112
+ 'name and nothing else.',
144
113
  '',
145
114
  '--- opened with ---',
146
115
  cut(item?.title, 300),
147
- cut(item?.body, 1200),
116
+ // A teammate's run is titled with its own part of a message that may have
117
+ // woken several (shared/teammates.mjs `runTitle`); the whole would mix them.
118
+ (item?.labels ?? []).some((l) => String(l).startsWith('teammate:')) ? '' : cut(item?.body, 1200),
148
119
  '--- latest ---',
149
120
  latest || '(nothing written yet)',
150
121
  '---',
@@ -0,0 +1,13 @@
1
+ // WHAT A RUN CHANGED: the checkout read back against its photograph, then the
2
+ // change file written. Git commands and a whole transcript, so it runs on the
3
+ // git thread (main/git-change-offthread.mjs), not on the app's main thread.
4
+ import { changeFromRepo } from './git-change.mjs';
5
+ import { writeChangeForRun } from './code-change.mjs';
6
+
7
+ export function saveRunChange({ repoBefore = null, ...run }) {
8
+ let repos = [];
9
+ try {
10
+ if (repoBefore) repos = [changeFromRepo(repoBefore)];
11
+ } catch (e) { console.warn('zero: could not read the repository back:', e.message); }
12
+ return writeChangeForRun({ ...run, repos });
13
+ }