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,323 @@
1
+ // WHAT ACTUALLY MOVED ON THE DISK, READ OUT OF THE REPOSITORY.
2
+ //
3
+ // THE HOLE. main/code-change.mjs builds a run's change out of the agent's own
4
+ // conversation, which only carries the before and after when the agent used
5
+ // the Edit or Write tool. Measured on this Mac 2026-08-24: of 105 runs that
6
+ // changed a file over two days, 4 would have shown the whole change. The other
7
+ // 101 rewrote their files with sed, a heredoc or a script, and a card drawn
8
+ // from the conversation has nothing in it. That is what David was clicking.
9
+ //
10
+ // SO THE CARD STOPS ASKING HOW THE AGENT TYPED. A run's checkout is
11
+ // photographed when the session spawns and read again when it exits, and the
12
+ // difference between the two is the change. It catches every route in, because
13
+ // it never asks which one was taken.
14
+ //
15
+ // WHY THE CONVERSATION IS STILL READ AND NOT REPLACED. Three reasons, all
16
+ // real: a product with no repository (most of her twenty-eight) has nothing
17
+ // here to read; the product's own docs folder is not a checkout and that is
18
+ // where the reports and drawings a run makes for her live; and the agent's
19
+ // sentence beside a hunk only exists in the conversation. So git fills the
20
+ // code and the transcript fills the words, and where git has a file the
21
+ // transcript also has, git wins: it is the disk.
22
+ //
23
+ // WHAT IT STILL CANNOT DO, said plainly because the next session will wonder.
24
+ // In a checkout two agents share, a file the other one wrote inside this run's
25
+ // window is inside this run's window, and no reading of the disk can tell them
26
+ // apart. Most workers get their own worktree, where this does not arise.
27
+ import { execFileSync } from 'node:child_process';
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+
32
+ /** Bigger than this is not a file anybody reads a diff of. */
33
+ const BIG_FILE = 512 * 1024;
34
+
35
+ /** How much already-changed work we copy at spawn before giving up on it. */
36
+ const COPY_BUDGET = 16 * 1024 * 1024;
37
+
38
+ /** Rows one file may contribute before the rest is left out and said so. */
39
+ const MAX_ROWS_PER_FILE = 20_000;
40
+
41
+ /** The store's own bookkeeping, which git would not offer anyway. */
42
+ const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
43
+
44
+ function git(cwd, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
45
+ return execFileSync('git', ['--no-pager', ...args], {
46
+ cwd,
47
+ encoding: 'utf8',
48
+ maxBuffer,
49
+ stdio: ['ignore', 'pipe', 'ignore'],
50
+ });
51
+ }
52
+
53
+ function tryGit(cwd, args, opts) {
54
+ try { return git(cwd, args, opts); } catch { return null; }
55
+ }
56
+
57
+ /** Split a -z list, dropping the empty tail. */
58
+ function zList(out) {
59
+ return String(out ?? '').split('\0').filter(Boolean);
60
+ }
61
+
62
+ /** Text we would draw. A NUL in the first block means a picture or a binary. */
63
+ function readTextFile(file) {
64
+ let stat;
65
+ try { stat = fs.statSync(file); } catch { return null; }
66
+ if (!stat.isFile() || stat.size > BIG_FILE) return null;
67
+ let text;
68
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
69
+ if (text.includes('\0')) return null;
70
+ return text;
71
+ }
72
+
73
+ /**
74
+ * The checkout as it stands, kept so the same checkout can be read again when
75
+ * the run ends.
76
+ *
77
+ * Cheap on purpose, because it runs on the spawn path of every worker: two
78
+ * git commands that list names, and a copy of only those files that were
79
+ * already changed before the run started. In a worker's own fresh worktree
80
+ * that last set is empty. Measured on her main checkout, which is the worst
81
+ * case she has: 199 untracked files, 4 already changed, 0.02s.
82
+ *
83
+ * Returns null when the directory is not a repository at all, which is the
84
+ * ordinary case for a product with no code, and the caller carries on with
85
+ * the conversation alone.
86
+ */
87
+ export function snapshotRepo(cwd) {
88
+ if (!cwd) return null;
89
+ const root = tryGit(cwd, ['rev-parse', '--show-toplevel'])?.trim();
90
+ if (!root) return null;
91
+ const head = tryGit(root, ['rev-parse', 'HEAD'])?.trim() || null;
92
+
93
+ // Everything that was ALREADY different from HEAD when we arrived, plus
94
+ // everything untracked. Their contents are the only thing we have to keep,
95
+ // because for every other file the start state is the commit.
96
+ const dirty = head ? zList(tryGit(root, ['diff', '--name-only', '-z', 'HEAD'])) : [];
97
+ const others = zList(tryGit(root, ['ls-files', '-z', '--others', '--exclude-standard']));
98
+
99
+ const before = new Map();
100
+ let kept = 0;
101
+ for (const rel of new Set([...dirty, ...others])) {
102
+ if (NOT_CODE.test(rel)) continue;
103
+ const text = readTextFile(path.join(root, rel));
104
+ // A file that is missing, binary or huge is remembered as unreadable
105
+ // rather than as empty: calling it empty would draw the whole of it green
106
+ // at the end as if this run had written it.
107
+ if (text === null) { before.set(rel, null); continue; }
108
+ if (kept + text.length > COPY_BUDGET) { before.set(rel, null); continue; }
109
+ kept += text.length;
110
+ before.set(rel, text);
111
+ }
112
+ return { root, head, before, at: Date.now() };
113
+ }
114
+
115
+ /**
116
+ * WHERE A HUNK STARTS IN EACH FILE, off its `@@` header —.
117
+ *
118
+ * Measured while looking into that: the pane had no line numbers at all, and
119
+ * the reason is that this parser matched `@@` only to know a hunk had started
120
+ * and threw the header away, numbers included. They were always right there.
121
+ *
122
+ * `@@ -12,7 +14,9 @@ some context` means the old file's run starts at line 12
123
+ * and the new file's at 14. The counts after the commas are how many lines each
124
+ * run covers and are not needed: walking the body gives the same answer and
125
+ * cannot disagree with the rows actually drawn.
126
+ */
127
+ function hunkStarts(header) {
128
+ const m = /^@@+ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(String(header ?? ''));
129
+ return m ? { old: Number(m[1]), now: Number(m[2]) } : null;
130
+ }
131
+
132
+ /**
133
+ * One line of a change, the shape renderer/src/code-artifact.ts draws, and the
134
+ * line number each one had in each file.
135
+ *
136
+ * `nums` runs parallel to `rows`, one `[old, now]` per row, with null on the
137
+ * side a row does not exist in: a removed line has no number in the new file
138
+ * and an added one has none in the old. It is a SECOND array rather than a
139
+ * third slot on the row because `Row` is `['=' | '+' | '-', string]` in six
140
+ * other files and their tests, and widening it to carry a number that only
141
+ * git can supply would touch every one of them to no purpose.
142
+ *
143
+ * With no header there are no numbers, and that is left honest rather than
144
+ * guessed: `nums` comes back null and the pane draws the gutter empty.
145
+ */
146
+ function rowsFromPatchBody(body, starts) {
147
+ const rows = [];
148
+ const nums = starts ? [] : null;
149
+ let old = starts?.old ?? 0;
150
+ let now = starts?.now ?? 0;
151
+ for (const line of body) {
152
+ const mark = line[0];
153
+ if (mark === '\\') continue; // ""
154
+ if (mark === '+') { rows.push(['+', line.slice(1)]); nums?.push([null, now++]); }
155
+ else if (mark === '-') { rows.push(['-', line.slice(1)]); nums?.push([old++, null]); }
156
+ else if (mark === ' ') { rows.push(['=', line.slice(1)]); nums?.push([old++, now++]); }
157
+ }
158
+ return { rows, nums };
159
+ }
160
+
161
+ /**
162
+ * A unified diff as files and hunks.
163
+ *
164
+ * Git's own diff rather than our line matcher, because a whole file read off
165
+ * the disk is thousands of lines and the matcher in code-change.mjs gives up
166
+ * over four million cells, which a 3,000 line file crosses on its own. Git
167
+ * also brings the three lines of context that make a save able to find its
168
+ * place again (renderer/src/code-edit.ts).
169
+ */
170
+ export function filesFromPatch(patch) {
171
+ const out = new Map();
172
+ let file = null;
173
+ let oldName = null;
174
+ let hunk = null;
175
+ // The `@@` line for the hunk being collected, kept until its body is closed.
176
+ let starts = null;
177
+ const closeHunk = () => {
178
+ if (!file || !hunk) return;
179
+ const { rows, nums } = rowsFromPatchBody(hunk, starts);
180
+ hunk = null;
181
+ starts = null;
182
+ const plus = rows.filter((r) => r[0] === '+').length;
183
+ const minus = rows.filter((r) => r[0] === '-').length;
184
+ if (!plus && !minus) return;
185
+ file.hunks.push(nums ? { rows, nums, plus, minus } : { rows, plus, minus });
186
+ file.plus += plus;
187
+ file.minus += minus;
188
+ };
189
+ const nameOn = (line, side) => {
190
+ const name = line.slice(4).trim();
191
+ if (name === '/dev/null') return null;
192
+ return name.replace(side === 'old' ? /^a\// : /^b\//, '');
193
+ };
194
+ for (const line of String(patch ?? '').split('\n')) {
195
+ if (line.startsWith('diff --git ')) { closeHunk(); file = null; oldName = null; continue; }
196
+ if (line.startsWith('--- ')) { closeHunk(); oldName = nameOn(line, 'old'); file = null; continue; }
197
+ if (line.startsWith('+++ ')) {
198
+ // A delete has no new name, so it is filed under the old one. A rename
199
+ // is filed under the new one, which is where it now is on the disk.
200
+ const rel = nameOn(line, 'new') ?? oldName;
201
+ file = null;
202
+ if (rel && !NOT_CODE.test(rel)) {
203
+ file = out.get(rel) ?? { path: rel, hunks: [], plus: 0, minus: 0 };
204
+ out.set(rel, file);
205
+ }
206
+ continue;
207
+ }
208
+ if (line.startsWith('@@')) { closeHunk(); hunk = file ? [] : null; starts = file ? hunkStarts(line) : null; continue; }
209
+ if (hunk) hunk.push(line);
210
+ }
211
+ closeHunk();
212
+ // A file git listed but whose whole diff was binary says nothing here.
213
+ return [...out.values()].filter((f) => f.hunks.length);
214
+ }
215
+
216
+ /** A file that was not there before: every line of it, green. */
217
+ function wholeFileRows(text) {
218
+ const parts = String(text).split('\n');
219
+ if (parts.length > 1 && parts[parts.length - 1] === '') parts.pop();
220
+ return parts.map((l) => ['+', l]);
221
+ }
222
+
223
+ /** Two texts, diffed by git without either of them being in a repository. */
224
+ function patchBetweenTexts(before, after) {
225
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-diff-'));
226
+ try {
227
+ const a = path.join(dir, 'before');
228
+ const b = path.join(dir, 'after');
229
+ fs.writeFileSync(a, before);
230
+ fs.writeFileSync(b, after);
231
+ // --no-index exits 1 when the files differ, which is the whole point, so
232
+ // the non-zero exit is the success case and is read off the error.
233
+ try {
234
+ return git(dir, ['diff', '--no-color', '--no-ext-diff', '--no-index', '-U3', '--', a, b]);
235
+ } catch (e) {
236
+ return String(e?.stdout ?? '');
237
+ }
238
+ } finally {
239
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
240
+ }
241
+ }
242
+
243
+ /**
244
+ * What this run changed in this checkout, read off the disk.
245
+ *
246
+ * Three sources, because a checkout has three kinds of file in it and the
247
+ * start state of each is somewhere different:
248
+ *
249
+ * - Tracked and untouched at spawn: the commit we were on is the before, so
250
+ * one `git diff` against it says everything, committed work included.
251
+ * - Already changed at spawn: the copy we kept is the before, because the
252
+ * commit would credit this run with somebody else's unfinished work.
253
+ * - Written by this run and never tracked: there is no before, so the file is
254
+ * every line green.
255
+ */
256
+ export function changeFromRepo(snapshot) {
257
+ if (!snapshot?.root) return { files: [], plus: 0, minus: 0, root: snapshot?.root ?? null };
258
+ const { root, head, before } = snapshot;
259
+ const files = new Map();
260
+ const add = (file) => {
261
+ if (!file.hunks.length) return;
262
+ if (file.hunks.length && file.plus + file.minus > MAX_ROWS_PER_FILE) {
263
+ // Kept honest rather than kept whole: a generated file can be tens of
264
+ // thousands of lines and nobody reads past the first screen anyway.
265
+ let rows = 0;
266
+ const keep = [];
267
+ for (const h of file.hunks) {
268
+ if (rows >= MAX_ROWS_PER_FILE) break;
269
+ keep.push(h);
270
+ rows += h.rows.length;
271
+ }
272
+ file.leftOut = file.hunks.length - keep.length;
273
+ file.hunks = keep;
274
+ }
275
+ files.set(file.path, file);
276
+ };
277
+
278
+ // 1. Everything tracked, against the commit the run started on. The files
279
+ // we kept a copy of are cut out here and done properly below.
280
+ if (head) {
281
+ const excludes = [...before.keys()].map((rel) => `:(exclude,literal)${rel}`);
282
+ const patch = tryGit(root, [
283
+ 'diff', '--no-color', '--no-ext-diff', '-U3', '-M', head, '--', '.', ...excludes,
284
+ ]);
285
+ for (const file of filesFromPatch(patch ?? '')) add(file);
286
+ }
287
+
288
+ // 2. The files that were already changed when we arrived.
289
+ for (const [rel, kept] of before) {
290
+ if (kept === null) continue; // unreadable at spawn; we cannot honestly diff it
291
+ const now = readTextFile(path.join(root, rel));
292
+ const after = now === null ? '' : now;
293
+ if (after === kept) continue;
294
+ for (const file of filesFromPatch(patchBetweenTexts(kept, after))) {
295
+ add({ ...file, path: rel });
296
+ }
297
+ }
298
+
299
+ // 3. Files this run made that git has never tracked.
300
+ for (const rel of zList(tryGit(root, ['ls-files', '-z', '--others', '--exclude-standard']))) {
301
+ if (before.has(rel) || files.has(rel) || NOT_CODE.test(rel)) continue;
302
+ const text = readTextFile(path.join(root, rel));
303
+ if (text === null || !text.length) continue;
304
+ const rows = wholeFileRows(text);
305
+ // A FILE NOBODY HAS SEEN BEFORE NUMBERS ITSELF. Every row is an addition
306
+ // and the file starts at line 1, so there is nothing to parse and no reason
307
+ // to leave this the one place in the pane with an empty gutter.
308
+ const nums = rows.map((_, i) => [null, i + 1]);
309
+ add({ path: rel, hunks: [{ rows, nums, plus: rows.length, minus: 0 }], plus: rows.length, minus: 0 });
310
+ }
311
+
312
+ const out = [...files.values()];
313
+ return {
314
+ files: out,
315
+ plus: out.reduce((n, f) => n + f.plus, 0),
316
+ minus: out.reduce((n, f) => n + f.minus, 0),
317
+ root,
318
+ // When the photograph was taken, which is the run's own start and the only
319
+ // honest answer to "how long did this take" for a run whose conversation
320
+ // carried no edits at all.
321
+ at: snapshot.at,
322
+ };
323
+ }
@@ -0,0 +1,90 @@
1
+ // THE ORIGIN A PICTURE IN THE APP'S OWN WINDOW IS LOADED FROM.
2
+ //
3
+ // WHY, MEASURED RATHER THAN GUESSED. A pasted screenshot goes to disk the
4
+ // moment it is pasted, so the thumbnail beside the reply box stopped carrying
5
+ // bytes and started carrying a path, drawn as `<img
6
+ // src="file:///…/.staging/…png">`. Her Agentbox runs from the dev server —
7
+ // `ZERO_DEV_URL=http://localhost:5199` on the process that was up when this was
8
+ // written — and an http page may not touch file://. The same probe, same
9
+ // picture, same window settings, two origins:
10
+ //
11
+ // page at file:///… naturalWidth 788
12
+ // page at http://127.0.0.1/… naturalWidth 0
13
+ // console: "Not allowed to load local resource: file:///…"
14
+ //
15
+ // So every pasted thumbnail is a broken image in a dev-served window, and so is
16
+ // every screenshot a worker put in a message, because `ArtifactImg` in
17
+ // `Focus.tsx` builds file:// urls too. A packaged app is loaded with loadFile
18
+ // and does not hit it, which is why it survived so long: the app she uses all
19
+ // day is the one build that cannot draw its own pictures.
20
+ //
21
+ // THE FIX IS AN ORIGIN, NOT A FLAG. Turning webSecurity off would fix it and
22
+ // would also let any page in this window read the disk, which is not a trade
23
+ // worth making for a thumbnail. Instead the renderer asks for pictures on a
24
+ // scheme of the app's own, registered standard and secure, and this decides
25
+ // what may come back. Same shape as main/doc-scheme.mjs, and deliberately NOT
26
+ // the same scheme: that one serves whole documents an agent wrote, under grants
27
+ // the pane opens one at a time, and this one serves her pictures wherever they
28
+ // sit. Two answers to two different questions.
29
+ //
30
+ // WHAT MAY BE SERVED, and it is two rules that both have to pass:
31
+ //
32
+ // 1. It is a picture. An extension off the list below, nothing else. A url is
33
+ // the only thing between a page and the disk, so a scheme that can hand
34
+ // out any file is a scheme that can hand out `zero.config.json`.
35
+ // 2. It is inside a root the app owns: the account root (which holds every
36
+ // product's docs, its attachments, its designs and the `.staging` folder a
37
+ // paste lands in) or a product's own code repo, which is where a worker
38
+ // writes `shots/…` and then names it on a card.
39
+ import path from 'node:path';
40
+ import { IMG_SCHEME, IMG_SCHEMES, isScheme } from '../shared/schemes.mjs';
41
+
42
+ // The name is read rather than typed, and every spelling this app has used is
43
+ // still answered: a product's logo is kept in the store as a url on this
44
+ // scheme. shared/schemes.mjs carries that argument in full.
45
+ export { IMG_SCHEME, IMG_SCHEMES };
46
+
47
+ // One host, so every picture in the window shares one origin. Same reason
48
+ // doc-scheme keeps one: a page and the picture beside it must never look
49
+ // cross-origin to each other.
50
+ export const IMG_HOST = 'file';
51
+
52
+ const PICTURES = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.bmp', '.ico']);
53
+
54
+ /** The url the renderer draws, for a picture already resolved on disk. */
55
+ export function imgUrl(file) {
56
+ if (typeof file !== 'string' || !path.isAbsolute(file)) return null;
57
+ // Segment by segment: encodeURIComponent would eat the separators, and
58
+ // encodeURI would leave '#' and '?' in a filename to be read as url syntax.
59
+ const parts = file.split('/').filter(Boolean).map(encodeURIComponent);
60
+ return `${IMG_SCHEME}://${IMG_HOST}/${parts.join('/')}`;
61
+ }
62
+
63
+ /** The file a request under this scheme is asking for, or null if it is not one. */
64
+ export function imgPath(url) {
65
+ let parsed;
66
+ try { parsed = new URL(String(url)); } catch { return null; }
67
+ if (!isScheme(IMG_SCHEMES, parsed.protocol)) return null;
68
+ let file;
69
+ try { file = decodeURIComponent(parsed.pathname); } catch { return null; }
70
+ if (!file.startsWith('/')) return null;
71
+ // A climb written into the url is normalised away here rather than trusted to
72
+ // the root check below.
73
+ const normal = path.normalize(file);
74
+ return path.isAbsolute(normal) ? normal : null;
75
+ }
76
+
77
+ /** Is `file` inside `dir`, without /a/bc counting as inside /a/b. */
78
+ function inside(dir, file) {
79
+ if (!dir || !file) return false;
80
+ const d = path.resolve(dir);
81
+ const f = path.resolve(file);
82
+ return f === d || f.startsWith(d + path.sep);
83
+ }
84
+
85
+ /** Is this a picture the window is allowed to draw? Pure, so it is testable. */
86
+ export function servable(file, roots) {
87
+ if (typeof file !== 'string' || !path.isAbsolute(file)) return false;
88
+ if (!PICTURES.has(path.extname(file).toLowerCase())) return false;
89
+ return (roots ?? []).some((root) => inside(root, file));
90
+ }
@@ -0,0 +1,205 @@
1
+ // A fixed allowlist exposes the instruction files, never arbitrary paths.
2
+ // Defaults are shipped separately so an edited live file can always be reset.
3
+ //
4
+ // THERE ARE THREE OF THEM NOW, NOT FOUR (w-3dc46f3a67, 2026-09-22). Hers, on
5
+ // being shown the five prompts a person could edit: "Those five layers of
6
+ // prompts that the user can adjust might be overkill. Maybe we decide not to
7
+ // expose some of them, or maybe we would best aggregate others."
8
+ //
9
+ // A person opening that page is answering two questions, what should agents do
10
+ // and how should they talk to me, so the writing rules and the finishing rules
11
+ // became ONE document: `message-rules.md`, "How agents write to you". The
12
+ // evidence they were one thing all along is her own rules file, where three of
13
+ // four paragraphs are about how agents write to her rather than what they do.
14
+ // `joinMessageRules` below carries the two old files into the new one.
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import {fileURLToPath} from 'node:url';
18
+ // Every line this file prints is prefixed with the app's own name, read from the
19
+ // one place that holds it. Typing it here is what left three dead names in the
20
+ // code through two renames (tests/the-app-is-named-in-one-place).
21
+ import {nameSlug} from '../shared/product-name.mjs';
22
+ // ADHD MODE IS A FOURTH FILE, NOT A PARAGRAPH INSIDE THE THIRD (w-5737fe67cf,
23
+ // 2026-09-25). Hers: "it should really just append that text to your content if
24
+ // you have ADHD mode on, and then remove it when you turn it off... Maybe it
25
+ // opens a new section with additional instructions that get applied." Kept in
26
+ // its own file so her edits to it never mix with her own rules, and survive
27
+ // while the mode is off. Whether it is on lives in her config (`adhdMode`).
28
+ const files = {rules:'founder.md',messages:'message-rules.md',adhd:'adhd-mode.md',system:'worker.md'};
29
+ // The two names the joined document replaced, read only by the migration.
30
+ const WAS_MESSAGES = ['writing-rules.md','finishing.md'];
31
+ const defaults = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../shared/instruction-defaults.json',import.meta.url)),'utf8'));
32
+ function fileFor(id) {
33
+ if (!Object.hasOwn(files,id)) throw new Error('Unknown instruction section');
34
+ return files[id];
35
+ }
36
+ export function readInstruction(appDir,id,shippedDir=appDir) {
37
+ const file=fileFor(id);
38
+ const defaultText=id==='rules' ? '' : defaults[id];
39
+ let text=defaultText;
40
+ try {text=fs.readFileSync(path.join(appDir,'briefs',file),'utf8');} catch(e) {if(e.code!=='ENOENT') throw e; if(id==='system') text=readSystemTemplate(shippedDir,appDir);}
41
+ return {text,defaultText};
42
+ }
43
+ /**
44
+ * RESTORE POINTS, BECAUSE A SAVE USED TO BE THE END OF THE OLD TEXT
45
+ * (w-94b3af4e70, 2026-09-28).
46
+ *
47
+ * Hers, on why the page needs this at all: "a user might change the agent
48
+ * instructions and lose some critical information to the point where maybe the
49
+ * app doesn't work the way they want it or they think, 'It worked better
50
+ * before. I want to restore it.'"
51
+ *
52
+ * Until this, the only thing the page could hand back was our shipped text, and
53
+ * on her Mac that means deleting about 21,000 characters of her own writing to
54
+ * recover 5,654 of ours. The box saves 400ms after a keystroke, straight over
55
+ * the file, so a paragraph deleted at noon was gone at noon.
56
+ *
57
+ * WHAT COUNTS AS A VERSION: the text that was in the file before a save, kept
58
+ * under the time that text was itself saved, so the list reads as the versions
59
+ * she wrote rather than as a log of keystrokes. A burst of typing is one
60
+ * version and not two hundred, because text that has been sitting untouched for
61
+ * less than QUIET_MS is still being written and is not a place to come back to.
62
+ *
63
+ * It never throws into a save. Losing a restore point is a small thing and
64
+ * failing to write what she just typed is not.
65
+ */
66
+ const HISTORY='instruction-history';
67
+ const KEEP=20;
68
+ const QUIET_MS=10*60*1000;
69
+ function historyDir(appDir,id) {return path.join(appDir,HISTORY,id);}
70
+ export function listVersions(appDir,id) {
71
+ fileFor(id);
72
+ const dir=historyDir(appDir,id);
73
+ let names;
74
+ try {names=fs.readdirSync(dir);} catch(e) {if(e.code!=='ENOENT') throw e; return [];}
75
+ return names.filter(n=>n.endsWith('.md')).map(n=>{
76
+ const ts=Number(n.slice(0,-3));
77
+ if(!Number.isFinite(ts)) return null;
78
+ try {return {ts,chars:fs.readFileSync(path.join(dir,n),'utf8').length};} catch {return null;}
79
+ }).filter(Boolean).sort((a,b)=>b.ts-a.ts);
80
+ }
81
+ export function readVersion(appDir,id,ts) {
82
+ fileFor(id);
83
+ const at=Number(ts);
84
+ if(!Number.isFinite(at)) throw new Error('Unknown version');
85
+ return {text:fs.readFileSync(path.join(historyDir(appDir,id),`${at}.md`),'utf8')};
86
+ }
87
+ function keepVersion(appDir,id,now=Date.now()) {
88
+ try {
89
+ const live=path.join(appDir,'briefs',fileFor(id));
90
+ let stat,text;
91
+ try {stat=fs.statSync(live); text=fs.readFileSync(live,'utf8');}
92
+ catch(e) {if(e.code!=='ENOENT') throw e; return;}
93
+ if(now-stat.mtimeMs<QUIET_MS) return;
94
+ const ts=Math.floor(stat.mtimeMs);
95
+ const have=listVersions(appDir,id);
96
+ if(have.some(v=>v.ts===ts)) return;
97
+ if(have.length && readVersion(appDir,id,have[0].ts).text===text) return;
98
+ const dir=historyDir(appDir,id);
99
+ fs.mkdirSync(dir,{recursive:true});
100
+ fs.writeFileSync(path.join(dir,`${ts}.md`),text,'utf8');
101
+ for(const old of [{ts},...have].sort((a,b)=>b.ts-a.ts).slice(KEEP)) fs.rmSync(path.join(dir,`${old.ts}.md`),{force:true});
102
+ } catch(e) {console.warn(`${nameSlug}: could not keep a restore point for ${id}: ${e.message}`);}
103
+ }
104
+ export function writeInstruction(appDir,id,text,now=Date.now()) {
105
+ const file=fileFor(id);
106
+ if(typeof text!=='string') throw new Error('Instructions must be text');
107
+ keepVersion(appDir,id,now);
108
+ const target=path.join(appDir,'briefs',file);
109
+ fs.mkdirSync(path.dirname(target),{recursive:true});
110
+ const temporary=`${target}.tmp-${process.pid}`;
111
+ fs.writeFileSync(temporary,text,'utf8');
112
+ fs.renameSync(temporary,target);
113
+ return {ok:true};
114
+ }
115
+
116
+ // Installed bundles are read-only. User templates shadow the shipped brief.
117
+ export function readSystemTemplate(appDir,dataDir=appDir) {
118
+ for(const root of new Set([dataDir,appDir])) {
119
+ try {return fs.readFileSync(path.join(root,'briefs','worker.md'),'utf8');}
120
+ catch(e) {if(e.code!=='ENOENT') throw e;}
121
+ }
122
+ return defaults.system;
123
+ }
124
+
125
+ /**
126
+ * HER OWN WRITING, CARRIED OUT OF THE CHECKOUT ONCE (w-3dc46f3a67, 2026-09-21).
127
+ *
128
+ * Until this ran, the four boxes in Settings read and wrote `dataDir/briefs`,
129
+ * and running from source `dataDir` is the git checkout the whole fleet works
130
+ * in. Measured on her repo that day: briefs/founder.md had four commits, all of
131
+ * them an agent's, and the working copy matched the newest exactly. Her rules
132
+ * were being kept in the one folder every worker is allowed to `git checkout`.
133
+ *
134
+ * WHAT MOVES IS ONLY WHAT SHE WROTE, and the test for that is the shipped
135
+ * default. A file whose text IS the default is our file sitting where we put
136
+ * it, and copying it into her folder would freeze it there: the next release's
137
+ * worker brief would never reach her, because her "edit" would shadow it
138
+ * forever. So a file is carried across only when it differs from the default,
139
+ * which on her Mac is founder.md and writing-rules.md and neither of the two we
140
+ * ship. Those two are the same pair package.json keeps out of the download.
141
+ *
142
+ * It is a COPY, not a move, and it never overwrites. The checkout keeps its
143
+ * copy so that merging this cannot lose a word, and a second run finds the
144
+ * target already there and does nothing. Never throws: a migration that cannot
145
+ * run must not stop the app from starting.
146
+ *
147
+ * Returns the ids it carried, so a caller can say what really happened.
148
+ */
149
+ export function carryHerBriefsAcross(fromDir,toDir) {
150
+ const carried=[];
151
+ if(!fromDir||!toDir||path.resolve(fromDir)===path.resolve(toDir)) return carried;
152
+ for(const [id,file] of Object.entries(files)) {
153
+ try {
154
+ const target=path.join(toDir,'briefs',file);
155
+ if(fs.existsSync(target)) continue;
156
+ let text;
157
+ try {text=fs.readFileSync(path.join(fromDir,'briefs',file),'utf8');}
158
+ catch(e) {if(e.code!=='ENOENT') console.warn(`${nameSlug}: could not read ${file}: ${e.message}`); continue;}
159
+ if(text===(id==='rules' ? '' : defaults[id])) continue;
160
+ writeInstruction(toDir,id,text);
161
+ carried.push(id);
162
+ } catch(e) {console.warn(`${nameSlug}: could not carry ${file} into the data folder: ${e.message}`);}
163
+ }
164
+ return carried;
165
+ }
166
+
167
+ /**
168
+ * THE TWO OLD MESSAGE FILES, JOINED INTO ONE, ONCE (w-3dc46f3a67, 2026-09-22).
169
+ *
170
+ * `writing-rules.md` said how an agent writes to her during a task and
171
+ * `finishing.md` said how it writes the last message. They are one box now, so
172
+ * they have to become one document, and this is where that happens: on the
173
+ * first boot after the change, before anything reads the new name.
174
+ *
175
+ * WHAT IT DOES WITH THE HALF SHE NEVER WROTE. The writing rules ship empty, so
176
+ * anything in that file is hers. The finishing rules ship with 5,794 characters
177
+ * of text, so most people have never touched them. When she has the one and not
178
+ * the other, the joined document takes the shipped finishing text as its second
179
+ * half rather than leaving it out: her copy shadows the default from here on,
180
+ * and dropping it would quietly stop every task ending the way it does today.
181
+ *
182
+ * AND WHEN SHE WROTE NEITHER, IT WRITES NOTHING AT ALL. That is the fresh
183
+ * install, and leaving her with no file of her own is what keeps a later
184
+ * release's default reaching her. Same rule as `carryHerBriefsAcross` above:
185
+ * never freeze our own text into her folder.
186
+ *
187
+ * Never throws, never overwrites. Returns what it joined, or null.
188
+ */
189
+ export function joinMessageRules(shippedDir,toDir) {
190
+ try {
191
+ const target=path.join(toDir,'briefs',files.messages);
192
+ if(fs.existsSync(target)) return null;
193
+ const read=(dir,name)=>{try {return fs.readFileSync(path.join(dir,'briefs',name),'utf8');} catch {return null;}};
194
+ const either=(name)=>read(toDir,name) ?? read(shippedDir,name);
195
+ const [writing,finishing]=WAS_MESSAGES.map((name)=>(either(name) ?? '').trim());
196
+ if(!writing && !finishing) return null;
197
+ const ending=finishing || defaults.messages.trim();
198
+ const joined=[writing,ending].filter(Boolean).join('\n\n');
199
+ writeInstruction(toDir,'messages',`${joined}\n`);
200
+ return {writing:writing.length,ending:ending.length,total:joined.length+1};
201
+ } catch(e) {
202
+ console.warn(`${nameSlug}: could not join the message rules: ${e.message}`);
203
+ return null;
204
+ }
205
+ }