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,68 @@
1
+ // STAGING A FILE FOR A REPLY BOX, before it is sent: the bytes of a paste, or
2
+ // a copy of a picture dropped in, written into the account's .staging folder,
3
+ // which the window may draw from (main/ipc.mjs `zero:stage-attachment` says
4
+ // why). A picture the window cannot draw is turned into one that it can here,
5
+ // so the box, the message and the teammate all get a picture.
6
+ import { execFile } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { turnedName } from '../shared/picture-formats.mjs';
10
+
11
+ const WEEK = 7 * 24 * 60 * 60 * 1000;
12
+ const SIPS_MS = 30_000;
13
+
14
+ /**
15
+ * Answers `{ path }`, and `name` too when the file was turned into another
16
+ * format, which is the name it goes on under.
17
+ */
18
+ export async function stageAttachment(dir, { name, dataBase64, srcPath } = {}, { turn = turnIntoPicture } = {}) {
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ // Sweep on the way in rather than on a timer: an abandoned paste is only
21
+ // ever discovered by the next one, and a week is longer than any draft has
22
+ // been left open. Failure to sweep must never fail the paste.
23
+ try {
24
+ const week = Date.now() - WEEK;
25
+ for (const f of fs.readdirSync(dir)) {
26
+ const p = path.join(dir, f);
27
+ try { if (fs.statSync(p).mtimeMs < week) fs.unlinkSync(p); } catch { /* leave it */ }
28
+ }
29
+ } catch { /* a sweep that cannot run is not a paste that failed */ }
30
+ const safe = String(name ?? 'pasted.png').replace(/[^\w.-]+/g, '-').slice(0, 80);
31
+ const abs = path.join(dir, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`);
32
+ // A path is a drag, bytes are a paste. Already inside the folder we serve
33
+ // from is left alone: copying it would only make a second file to sweep.
34
+ if (typeof srcPath === 'string' && srcPath) {
35
+ if (path.resolve(srcPath) === path.resolve(path.join(dir, path.basename(srcPath)))) return { path: srcPath };
36
+ fs.copyFileSync(srcPath, abs);
37
+ } else {
38
+ fs.writeFileSync(abs, Buffer.from(String(dataBase64 ?? ''), 'base64'));
39
+ }
40
+ const as = turnedName(name ?? safe);
41
+ if (!as) return { path: abs };
42
+ // One that cannot be turned goes as the file it is, which is how every TIFF went before.
43
+ const drawn = await Promise.resolve().then(() => turn(abs)).catch(() => null);
44
+ if (!drawn) return { path: abs };
45
+ try { fs.unlinkSync(abs); } catch { /* swept within the week */ }
46
+ return { path: drawn, name: as };
47
+ }
48
+
49
+ /**
50
+ * Turns a TIFF or HEIC into a PNG or JPEG beside it with the Mac's own sips,
51
+ * and answers the new file's path. Null off a Mac, for any other file, or
52
+ * when sips fails or writes nothing.
53
+ */
54
+ export async function turnIntoPicture(file, { platform = process.platform, run = sips } = {}) {
55
+ const as = turnedName(path.basename(file));
56
+ if (!as || platform !== 'darwin') return null;
57
+ const out = path.join(path.dirname(file), as);
58
+ try {
59
+ await run(['-s', 'format', as.endsWith('.png') ? 'png' : 'jpeg', file, '--out', out]);
60
+ return fs.statSync(out).size > 0 ? out : null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ const sips = (args) => new Promise((resolve, reject) => {
67
+ execFile('/usr/bin/sips', args, { timeout: SIPS_MS }, (err) => (err ? reject(err) : resolve()));
68
+ });
@@ -0,0 +1,50 @@
1
+ // WHICH REQUEST HELD THE APP. A spinning wheel on a Mac is the main process
2
+ // not answering, and nothing used to record what it was doing instead. This
3
+ // times every request the window makes and keeps the longest freeze; the
4
+ // memory log writes both into its minute line (main/memory-log.mjs).
5
+ //
6
+ // Only the synchronous part of a handler is timed. That is the part that holds
7
+ // the process: once a handler waits on a promise the window is free again.
8
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
9
+
10
+ export const SLOW_MS = 100;
11
+
12
+ export function stallWatch({ clock = () => performance.now(), loop = monitorEventLoopDelay({ resolution: 20 }) } = {}) {
13
+ loop.enable();
14
+ let slow = new Map();
15
+ let freezes = [];
16
+ return {
17
+ /** A freeze main/freeze-recorder.mjs caught, with the function that held it. */
18
+ noteFreeze({ ms, where }) {
19
+ freezes.push({ ms, where });
20
+ },
21
+ time(channel, fn) {
22
+ return (...args) => {
23
+ const start = clock();
24
+ try {
25
+ return fn(...args);
26
+ } finally {
27
+ const ms = Math.round(clock() - start);
28
+ if (ms >= SLOW_MS) {
29
+ const seen = slow.get(channel) ?? { channel, times: 0, maxMs: 0 };
30
+ seen.times += 1;
31
+ seen.maxMs = Math.max(seen.maxMs, ms);
32
+ slow.set(channel, seen);
33
+ }
34
+ }
35
+ };
36
+ },
37
+ /** The minute so far, and a fresh start. */
38
+ take() {
39
+ const out = {
40
+ longestStallMs: Math.round(loop.max / 1e6),
41
+ slowRequests: [...slow.values()].sort((a, b) => b.maxMs - a.maxMs).slice(0, 5),
42
+ ...(freezes.length ? { freezes: freezes.sort((a, b) => b.ms - a.ms).slice(0, 5) } : {}),
43
+ };
44
+ loop.reset();
45
+ slow = new Map();
46
+ freezes = [];
47
+ return out;
48
+ },
49
+ };
50
+ }
@@ -1,40 +1,17 @@
1
- // THE APP'S OWN HOME, so nothing Agentbox generates lands in the folder a worker
2
- // is standing in.
1
+ // The app's own home, so nothing Agentbox generates lands in the folder a
2
+ // worker runs in. Claude Code keeps per-project state under
3
+ // `~/.claude/projects/<encoded path>/` and Codex under `~/.codex`; this is the
4
+ // same convention. A product with no code repo makes its docs folder the
5
+ // worker's cwd (main/supervisor.mjs), so without this our records sat there.
3
6
  //
4
- // WHY THIS FILE EXISTS. A new user uploaded a file into her product folder,
5
- // watched a worker create a directory and delete things next to it, and asked
6
- // why it needed to.
7
+ // Our records move: the ledger, session traces, run records, repeat rules and
8
+ // parked writes. What describes the folder stays in it (`project.json`,
9
+ // `index.json` and `.lock`), so the folder still opens when copied to another
10
+ // Mac.
7
11
  //
8
- // MEASURED on her Mac 2026-08-24 and again 2026-08-26: Claude Code writes
9
- // nothing into a working directory. Per-project state goes to
10
- // `~/.claude/projects/<encoded path>/`, pasted files to `~/.claude/paste-cache`,
11
- // edit history to `~/.claude/file-history`. Codex does the same under `~/.codex`.
12
- // Agentbox did the opposite: a product with no code repo registered makes the
13
- // product docs directory the worker's own cwd (main/supervisor.mjs), which is
14
- // every new user's first product, and our ledger, session traces, run records
15
- // and repeat rules all sat in it.
16
- //
17
- // So this is the same convention, one name over: a dot-folder in her home
18
- // named after this app. On Linux a fresh install does not get that dot-folder,
19
- // and it does not get a visible folder of the app's name in $HOME. Application
20
- // data belongs in $XDG_DATA_HOME (see defaultStoreRoot). An existing dot-folder
21
- // is still read, so a store already on disk is not orphaned.
22
- //
23
- // WHAT MOVES AND WHAT DOES NOT, because the line is not "everything of ours".
24
- // The app's own RECORDS move: the work item ledger, session traces, run
25
- // records, repeat rules, parked writes. What DESCRIBES THE FOLDER ITSELF stays
26
- // in it: `project.json` says this directory is a product at all, `index.json`
27
- // catalogs the founder's own documents by their paths inside it, and `.lock`
28
- // guards that catalog. A folder you could copy to another Mac and still open is
29
- // worth more than four fewer files, and none of those three is what a worker
30
- // scribbles.
31
- //
32
- // WHAT THIS DOES NOT DO, said out loud so nobody reads it as more than it is.
33
- // It does not make the ledger unforgeable. A worker with Bash reaches that
34
- // folder exactly as easily as it reaches its own cwd. Claude Code has the
35
- // identical property and does not pretend otherwise. This moves our files out
36
- // of her folder; the grant that used to ride on the ledger is dealt with
37
- // separately, by not keeping it on the ledger at all (main/answer-modes.mjs).
12
+ // This does not protect the ledger: a worker with Bash reaches this folder as
13
+ // easily as its own cwd. The grant that used to sit on the ledger is handled in
14
+ // main/answer-modes.mjs.
38
15
 
39
16
  import fs from 'node:fs';
40
17
  import os from 'node:os';
@@ -45,18 +22,10 @@ import { Name, WAS, envNames, nameSlug, readEnv } from '../../shared/product-nam
45
22
  const isDir = (dir) => { try { return fs.statSync(dir).isDirectory(); } catch { return false; } };
46
23
 
47
24
  /**
48
- * THE STORE ROOT AS ENVIRONMENT, FOR EVERY PROCESS WE START, UNDER EVERY NAME
49
- * THIS APP HAS HAD.
50
- *
51
- * `appHome` above reads the old names, so anything shipped in this repo finds
52
- * the root whichever spelling it is given. What is NOT in this repo does not:
53
- * the store MCP server of an older install, her own launchd jobs, a script she
54
- * wrote. Those read the one variable they were written against, and a worker
55
- * whose store root is missing writes its whole session to nowhere, which is the
56
- * 2026-08-05 failure exactly.
57
- *
58
- * So both are exported. It costs one string per spawn and it makes the rename
59
- * invisible to everything outside this checkout.
25
+ * The store root under every env name this app has had, for every process we
26
+ * start. Code outside this repo (an older install's MCP server, a launchd job,
27
+ * a user's own script) reads only the name it was written against, and a
28
+ * worker with no store root writes its session nowhere.
60
29
  */
61
30
  export function storeRootEnv(storeRoot) {
62
31
  const env = {};
@@ -64,18 +33,10 @@ export function storeRootEnv(storeRoot) {
64
33
  return env;
65
34
  }
66
35
 
67
- // Where a config that names no store keeps the inbox.
68
- //
69
- // macOS, and anything that is not Linux: a folder of the app's name in the
70
- // home directory. That is the folder this app has used since 2026-09-22.
71
- // Linux: $XDG_DATA_HOME/<slug>, or ~/.local/share/<slug> when the variable is
72
- // unset, blank, or not an absolute path. A visible directory in $HOME is not
73
- // where an application writes. Measured 2026-10-07: a config that named no
74
- // store created one.
75
- //
76
- // The home and the environment are arguments so a test can ask about another
77
- // machine. A caller that passes a stand-in home must pass an environment that
78
- // belongs to it. This function does not decide that; it uses the env it is given.
36
+ // Where a config that names no store keeps the inbox. Anything but Linux: a
37
+ // folder of the app's name in the home directory. Linux: $XDG_DATA_HOME/<slug>,
38
+ // or ~/.local/share/<slug> when that is unset, blank or not absolute. A caller
39
+ // that passes a stand-in home must pass an env that belongs to it.
79
40
  export function defaultStoreRoot({ home = os.homedir(), platform = process.platform, env = process.env } = {}) {
80
41
  if (platform !== 'linux') return path.join(home, Name);
81
42
  const given = typeof env?.XDG_DATA_HOME === 'string' ? env.XDG_DATA_HOME.trim() : '';
@@ -83,21 +44,11 @@ export function defaultStoreRoot({ home = os.homedir(), platform = process.platf
83
44
  return path.join(base, nameSlug);
84
45
  }
85
46
 
86
- // One env var, for tests and for anyone who keeps their home somewhere else.
87
- // Read at call time rather than captured, because a test sets it per case.
88
- //
89
- // THE NAME IS READ HERE AND THE OLD NAMES STILL ANSWER. This was a literal
90
- // `ASTRAL_HOME` and a literal `.astral`, two renames after the app stopped
91
- // being called that, so both go through the name module now. The fallbacks are
92
- // not politeness: a launchd job, a login shell and any process started before a
93
- // rename still export the old variable, and a dot-folder written under an old
94
- // name holds a real store. The current name wins when both are there;
95
- // otherwise the newest old name that is actually on disk does, so nobody's
96
- // store goes missing on the release that renames the app.
97
- // `homeDir` is injected so a test can ask what this answers on a machine with
98
- // an old dot-folder on it, without one having to exist on the machine running
99
- // the test. `platform` and `env` are the same kind of injection: this suite
100
- // runs on a Mac and still has to answer a Linux home. Production passes neither.
47
+ // The env override is read at call time because a test sets it per case. Old
48
+ // env names and old dot-folders still answer: a launchd job or a process
49
+ // started before a rename exports the old variable, and an old dot-folder holds
50
+ // a real store. The current name wins, then the newest old name on disk.
51
+ // `homeDir`, `platform` and `env` are for tests; production passes none.
101
52
  export function appHome(homeDir = os.homedir(), { platform = process.platform, env } = {}) {
102
53
  const override = readEnv('HOME', env);
103
54
  if (override) return override;
@@ -107,11 +58,10 @@ export function appHome(homeDir = os.homedir(), { platform = process.platform, e
107
58
  const older = path.join(homeDir, `.${was.toLowerCase()}`);
108
59
  if (isDir(older)) return older;
109
60
  }
110
- // A fresh Linux directory has nothing of ours on disk yet. The answer has to
111
- // be the same path loadConfig opens, or the desktop and this lookup write two
112
- // stores. The machine's own XDG_DATA_HOME is an absolute path in the real
113
- // home; applying it to a stand-in home writes that stand-in's store there.
114
- // A caller that passes `env` is asking about that environment, not this one.
61
+ // A fresh Linux home has nothing of ours yet, so answer the path loadConfig
62
+ // opens, or the two write separate stores. This machine's XDG_DATA_HOME
63
+ // belongs to the real home only; a stand-in home or an explicit `env` does
64
+ // not get it.
115
65
  if (platform === 'linux') {
116
66
  const real = env === undefined && path.resolve(homeDir) === path.resolve(os.homedir());
117
67
  return defaultStoreRoot({ home: homeDir, platform: 'linux', env: real ? process.env : (env ?? {}) });
@@ -119,19 +69,15 @@ export function appHome(homeDir = os.homedir(), { platform = process.platform, e
119
69
  return here;
120
70
  }
121
71
 
122
- // Claude Code's own encoding, checked against a real folder on her Mac:
123
- // `/Users/you/Zero/accounts/00000.../agentbox` becomes
124
- // `-Users-you-Zero-accounts-00000...-agentbox`. Every character that is not
125
- // a letter or a digit becomes a dash, which is readable at a glance and safe on
126
- // every filesystem.
72
+ // Claude Code's own encoding: every character that is not a letter or a digit
73
+ // becomes a dash, so `/Users/you/a.b` becomes `-Users-you-a-b`.
127
74
  export function encodeProjectPath(dir) {
128
75
  return String(dir).replace(/[^A-Za-z0-9]/g, '-');
129
76
  }
130
77
 
131
- // The one thing that encoding cannot do: `/a/b.c` and `/a/b/c` encode the same.
132
- // Claude Code lives with that. We do not, because two products sharing one
133
- // ledger would silently merge two founders' inboxes, so the folder carries the
134
- // path it belongs to and a mismatch takes a short hash suffix instead.
78
+ // That encoding is lossy: `/a/b.c` and `/a/b/c` encode the same. Two products
79
+ // sharing one ledger would merge their inboxes, so each folder records the
80
+ // path it belongs to in `.origin` and a mismatch gets a short hash suffix.
135
81
  const ORIGIN = '.origin';
136
82
 
137
83
  function readOrigin(dir) {
@@ -139,28 +85,11 @@ function readOrigin(dir) {
139
85
  }
140
86
 
141
87
  /**
142
- * A MACHINERY DIRECTORY IS NOT A PROJECT DIRECTORY, and everything below has to
143
- * know the difference before it moves a byte.
144
- *
145
- * MEASURED on her Mac 2026-09-23 at 15:31:26, because this is not a hypothesis.
146
- * A session wrote a throwaway script to count stranded rows, listed
147
- * `<home>/projects`, and handed each of those directories to `readWorkItems` as
148
- * if it were a product folder. `readWorkItems` asks for the ledger, the ledger
149
- * asks for the machinery directory, and the machinery directory of a machinery
150
- * directory is a SECOND one nested inside the home. Then the sweep below did
151
- * what it is for and carried the ledger, the session traces, the run records,
152
- * the repeat rules and the parked writes one level down into it. Thirty eight
153
- * products in two seconds, and her inbox read empty until the folders were
154
- * carried back at 15:58.
155
- *
156
- * Nothing about that was exotic. Any agent with a shell can write that script,
157
- * and reading is supposed to be the safe thing to do. So the refusal lives
158
- * HERE, in the one place every caller passes through, rather than in a rule
159
- * about how to write scripts that nobody reading this will have been told.
160
- *
161
- * The test is the path itself: anything inside `<home>/projects` is ours
162
- * already. A product folder can never be one, because products live under the
163
- * account root and the home is a separate tree.
88
+ * True for a path inside `<home>/projects`, which is ours and never a product
89
+ * folder, since products live under the account root. Treating one as a
90
+ * product folder nests a second machinery directory inside it, and the sweep
91
+ * then moves every record one level down. Every caller passes through here, so
92
+ * this is where that is refused.
164
93
  */
165
94
  export function isMachineryDir(dir, home = appHome()) {
166
95
  const root = path.resolve(path.join(home, 'projects'));
@@ -170,10 +99,8 @@ export function isMachineryDir(dir, home = appHome()) {
170
99
  /** `<home>/projects/<encoded>` for this project directory. Creates nothing. */
171
100
  export function machineryDir(projectDir, home = appHome()) {
172
101
  const real = path.resolve(projectDir);
173
- // Already ours: it is its own answer. Encoding it a second time is the
174
- // 2026-09-23 fold, and returning it unchanged makes the mistake harmless
175
- // rather than silent, because a caller that guessed this path still gets the
176
- // right ledger back.
102
+ // Already a machinery directory: return it as is, so a caller that passed
103
+ // one still gets the right ledger rather than a nested one.
177
104
  if (isMachineryDir(real, home)) return real;
178
105
  const base = path.join(home, 'projects', encodeProjectPath(real));
179
106
  const claimed = readOrigin(base);
@@ -182,52 +109,26 @@ export function machineryDir(projectDir, home = appHome()) {
182
109
  return `${base}-${tag}`;
183
110
  }
184
111
 
185
- // EVERY NAME THAT IS OURS, so the move is ONE event rather than a slow leak.
186
- //
187
- // A path-by-path migration was the first shape and it is wrong for what this is
188
- // FOR. It moves a file the first time something asks for it, so the ledger goes
189
- // the moment the inbox opens and `sessions/` goes one item at a time, whenever a
190
- // trace happens to be read, which for a row nobody opens again is never.
191
- // Measured on her folder 2026-08-26: 1,785 of the 1,830 files that leave are
192
- // session traces. Lazily she would still be looking at nearly all of it. The
193
- // point is that her folder is clean, so all of it goes at once, the first time
194
- // this process touches the product.
112
+ // Every name that is ours. All of them move together when a product is first
113
+ // touched, not one at a time when read, because most of the files are session
114
+ // traces that nothing reads again.
195
115
  export const MACHINERY = ['work-items.jsonl', 'repeats.jsonl', 'sessions', 'runs', 'pending-writes'];
196
116
 
197
- // THE TWO OF OUR NAMES THAT ARE APPEND-ONLY LOGS. A stray one of these holds
198
- // lines the real file never got, so it is folded ONTO THE END of the real file
199
- // rather than moved over it. Everything else here is a whole file that stands
200
- // on its own.
117
+ // The append-only logs. A stray copy holds lines the real file never got, so
118
+ // it is appended onto the real file rather than moved over it.
201
119
  const LOGS = new Set(['work-items.jsonl', 'repeats.jsonl']);
202
120
 
203
121
  /**
204
- * The same directory, existing, with its origin marker written once, and every
205
- * record of ours carried in.
206
- *
207
- * THE SWEEP RUNS ON EVERY CALL, NOT ONCE PER PROCESS. It used to run once,
208
- * behind a `Set`, and `machineryPath` below only adopted a name when there was
209
- * NOTHING at the new place. Between them that meant the move happened exactly
210
- * once, and one of our files that appeared in her folder afterwards was never
211
- * picked up again: nothing read it, nothing moved it, and it sat there.
212
- *
213
- * MEASURED on her real folder the day the move landed, not reasoned about: two
214
- * release lines for two different cards written into a stray `work-items.jsonl`
215
- * at 14:10 and never folded in, and two run records stranded under `runs/`. So
216
- * this is not a theoretical window, it is where her folder actually was.
217
- *
218
- * The cost of running it every time is one stat per name against a path that is
219
- * normally absent. Five syscalls that find nothing is cheaper than being wrong,
220
- * and being wrong here is silent.
122
+ * The machinery directory for a project, created, with its `.origin` written
123
+ * once and every stray record of ours carried in. The sweep runs on every
124
+ * call, not once per process, because a file of ours can turn up in the
125
+ * product folder after the first move. It costs one stat per name.
221
126
  */
222
127
  export function ensureMachineryDir(projectDir, home = appHome()) {
223
128
  const dir = machineryDir(projectDir, home);
224
129
  fs.mkdirSync(dir, { recursive: true });
225
- // THE SWEEP IS WHAT DID THE DAMAGE, so it does not run on a directory that is
226
- // already ours. `machineryDir` hands back its own argument in that one case,
227
- // and there is nothing to carry across from a folder into itself. The origin
228
- // marker is left alone too: it names the product this machinery belongs to,
229
- // and overwriting it with the machinery's own path is how the folded copies
230
- // came to claim themselves.
130
+ // A directory that is already ours is not swept, and its `.origin` is left
131
+ // alone: overwritten with its own path, it would claim itself.
231
132
  if (dir === path.resolve(projectDir)) return dir;
232
133
  if (readOrigin(dir) === null) {
233
134
  try { fs.writeFileSync(path.join(dir, ORIGIN), `${path.resolve(projectDir)}\n`); } catch { /* best effort */ }
@@ -237,24 +138,18 @@ export function ensureMachineryDir(projectDir, home = appHome()) {
237
138
  }
238
139
 
239
140
  /**
240
- * One of our names, wherever it has turned up in her folder, carried across to
241
- * where it belongs. NOTHING IS EVER DESTROYED HERE.
141
+ * Carries one of our names from the product folder to the machinery directory.
142
+ * Nothing is ever destroyed here. In order:
242
143
  *
243
- * The rules, in the order they apply:
244
- *
245
- * - Nothing there: return. This is the ordinary case and it costs one stat.
246
- * - Nothing at the target yet: rename it across whole, which is what the
247
- * first move has always done.
248
- * - Both directories: merge them entry by entry by these same rules, then
249
- * drop the stray directory once the merge has emptied it.
144
+ * - Nothing there: return.
145
+ * - Nothing at the target: rename it across whole.
146
+ * - Both directories: merge entry by entry by these rules, then remove the
147
+ * stray directory if the merge emptied it.
250
148
  * - Both append-only logs: append the stray's bytes onto the real file and
251
- * delete the stray. Never the other way round; the real file is the one
252
- * with the history in it.
253
- * - Both ordinary files: the newer takes the name and the older is kept
254
- * beside it as `<name>.superseded-<mtime>` rather than deleted. That is the
255
- * run record case, and it is rare on purpose, because the writer of those
256
- * now writes straight here (main/code-change.mjs). A conflict only happens
257
- * while a process started before the move is still running.
149
+ * delete the stray.
150
+ * - Both ordinary files: the newer takes the name and the older is kept as
151
+ * `<name>.superseded-<mtime>`. This is rare, because run records are now
152
+ * written straight here (main/code-change.mjs).
258
153
  */
259
154
  export function foldStray(from, to, rel = path.basename(to)) {
260
155
  let stray;
@@ -267,14 +162,11 @@ export function foldStray(from, to, rel = path.basename(to)) {
267
162
  let entries = [];
268
163
  try { entries = fs.readdirSync(from); } catch { return false; }
269
164
  for (const name of entries) foldStray(path.join(from, name), path.join(to, name), name);
270
- // Only if the merge emptied it. A directory with anything left in it holds
271
- // something we did not understand, and it stays where it is.
272
165
  try { fs.rmdirSync(from); } catch { /* not empty, or gone already */ }
273
166
  return true;
274
167
  }
275
168
 
276
- // A directory on one side and a file on the other is not a case we have a
277
- // right answer for, so the stray is left alone and it is said out loud.
169
+ // A directory on one side and a file on the other has no right answer.
278
170
  if (stray.isDirectory() !== landed.isDirectory()) {
279
171
  console.warn(`${nameSlug}: ${from} and ${to} are not the same kind of thing; leaving the first alone`);
280
172
  return false;
@@ -307,23 +199,13 @@ export function foldStray(from, to, rel = path.basename(to)) {
307
199
  }
308
200
 
309
201
  /**
310
- * Where one of our records lives for this project, and the migration that gets
311
- * it there.
312
- *
313
- * THE MOVE HAPPENS ONCE, and `ensureMachineryDir` above has already done it by
314
- * the time this returns: anything still sitting in the product folder under one
315
- * of our names is RENAMED into the home. A rename on the same volume is atomic
316
- * and costs nothing, whether it is a 6 MB ledger or a directory of 1,785 session
317
- * traces, and it is undone by moving the file back.
318
- *
319
- * It is a move rather than a read-both-write-new because the ledger is an
320
- * append-only log: two halves of one log in two places is not a store, it is
321
- * two stores that disagree. Everything that reads it must therefore agree about
322
- * where it is, which is why the MCP server ships the same file.
202
+ * Where one of our records lives for this project, after any stray copy in the
203
+ * product folder has been renamed across. It is a move, not read-both-write-new,
204
+ * because two halves of an append-only ledger are two stores that disagree.
205
+ * The MCP server ships this same file so every reader agrees where it is.
323
206
  *
324
- * Races are ordinary here: a dozen processes may call this at once. Every
325
- * failure mode of the rename is benign and is swallowed — ENOENT means there
326
- * was nothing to move, EEXIST/ENOTEMPTY means somebody else moved it first.
207
+ * Many processes may call this at once. A failed rename is benign: ENOENT means
208
+ * nothing to move, EEXIST or ENOTEMPTY means another process moved it first.
327
209
  */
328
210
  export function machineryPath(projectDir, rel, home = appHome()) {
329
211
  if (typeof rel !== 'string' || !rel || path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) {
@@ -336,25 +218,14 @@ export function machineryPath(projectDir, rel, home = appHome()) {
336
218
  }
337
219
 
338
220
  /**
339
- * RECORDS A COPY WROTE UNDER THE WRONG HOME, CARRIED TO ITS OWN.
221
+ * Carries records a copy wrote under the wrong home over to `toHome`. The
222
+ * headless door (main/serve.mjs) once wrote its ledgers under whatever home it
223
+ * inherited, and this keeps the rows it wrote then.
340
224
  *
341
- * The headless door (main/serve.mjs) used to open the store without pointing
342
- * the app's home at `storeRoot`, so it wrote its ledgers under whatever home it
343
- * inherited: `~/.agentbox` for `npx agentbox-app`, and on the test Mac
344
- * 2026-10-01 the founder's own store, because the copies were started from an
345
- * agent session. Its workers were handed `storeRoot`, found nothing there, and
346
- * answered "no work item" to every store call. Pointing the home at `storeRoot`
347
- * fixes the next write; this is what keeps the rows already written from
348
- * vanishing on the first boot after the fix.
349
- *
350
- * ONLY WHAT IS PROVABLY THIS STORE'S MOVES. A machinery directory names the
351
- * product it belongs to in `.origin`; one is carried only when that product is
352
- * inside `storeRoot`. Everything else under `fromHome` is somebody else's
353
- * store and is not touched. Each record goes through `foldStray`, so a ledger
354
- * is appended rather than overwritten and nothing is ever destroyed; the old
355
- * directory is removed only once nothing but its marker is left in it.
356
- *
357
- * Returns the product folders whose records were carried.
225
+ * Only a machinery directory whose `.origin` is inside `storeRoot` moves;
226
+ * anything else under `fromHome` belongs to another store. Records go through
227
+ * `foldStray`, and the old directory is removed only when just its marker is
228
+ * left. Returns the product folders whose records were carried.
358
229
  */
359
230
  export function carryMisplacedMachinery(fromHome, storeRoot, toHome = storeRoot) {
360
231
  if (!fromHome || !storeRoot) return [];
@@ -381,29 +252,14 @@ export function carryMisplacedMachinery(fromHome, storeRoot, toHome = storeRoot)
381
252
  }
382
253
 
383
254
  /**
384
- * EVERY RECORD OF OURS FOR ONE PROJECT, GONE.
385
- *
386
- * This exists for exactly one caller: the practice project, which the founder
387
- * has now ruled must not survive its own tutorial. Deleting the folder in her
388
- * store and leaving the ledger, the session traces and the run records behind
389
- * would be a half removal, and it is the half nobody can see: MEASURED on her
390
- * real Mac 2026-08-28, the practice folder in her account held two files and
391
- * the machinery for it held 168 ledger lines and 29 session traces from six
392
- * walks.
393
- *
394
- * IT WILL ONLY EVER DELETE A DIRECTORY THAT SAYS IT BELONGS TO THIS PROJECT.
395
- * `.origin` is written by `ensureMachineryDir` on first use and holds the
396
- * resolved path of the project the directory is the machinery for; this refuses
397
- * unless it is there and it matches. A directory with no `.origin` is one we did
398
- * not make, or one made by a version that did not write the marker, and either
399
- * way it is somebody else's to delete. Same instinct as `foldStray` above: when
400
- * two things are not the same kind of thing, leave the first alone and say so.
401
- *
402
- * Never throws. A machinery directory that will not delete is a few kilobytes
403
- * under the app's own home; failing a launch over it would be the worse bug.
255
+ * Deletes every record of ours for one project. Its caller is the practice
256
+ * project, which must not outlive its tutorial; deleting only its folder would
257
+ * leave the ledger and traces behind.
404
258
  *
405
- * Returns { removed, why } so a caller can report what really happened rather
406
- * than assume.
259
+ * Deletes only a directory whose `.origin` matches this project. One with no
260
+ * marker was made by someone else or by an older version, and is left alone.
261
+ * Never throws, because a leftover directory is a few kilobytes and failing a
262
+ * launch over it would be worse. Returns { removed, why }.
407
263
  */
408
264
  export function removeMachinery(projectDir, home = appHome()) {
409
265
  const real = path.resolve(projectDir);
@@ -425,8 +281,8 @@ export function removeMachinery(projectDir, home = appHome()) {
425
281
  }
426
282
  }
427
283
 
428
- // Move one legacy path in, if it is there. Never throws: a store that cannot be
429
- // migrated must still open on the new path rather than fail to open at all.
284
+ // Moves one legacy path in, if it is there. Never throws, so a store that
285
+ // cannot be migrated still opens on the new path.
430
286
  export function adoptLegacy(from, to) {
431
287
  let there = false;
432
288
  try { there = fs.existsSync(from); } catch { return false; }
@@ -445,9 +301,8 @@ export function adoptLegacy(from, to) {
445
301
  }
446
302
  }
447
303
 
448
- // The store on one volume and the home on another. Rare, and a rename cannot
449
- // cross it, so copy and then remove — in that order, so a failure halfway
450
- // leaves the original where it was rather than nothing anywhere.
304
+ // A rename cannot cross volumes, so copy and then remove. In that order, a
305
+ // failure halfway leaves the original where it was.
451
306
  function copyAcross(from, to) {
452
307
  try {
453
308
  fs.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { MACHINERY } from './home.mjs';
3
4
 
4
5
  // The .git / .env deny-list, shared by EVERY creation-file access point: the
5
6
  // agent tools (main/agent/tools.mjs), the folder-backed materialize walk
@@ -31,6 +32,16 @@ export function deniesGitOrEnvPath(rel) {
31
32
  return GIT_PATH_RE.test(norm) || ENV_PATH_RE.test(norm);
32
33
  }
33
34
 
35
+ // True iff any segment of `rel` is a MACHINERY name (main/store/home.mjs). A
36
+ // document written to one of those names is swept into the real ledger by
37
+ // ensureMachineryDir, so the store write boundary must reserve them. Normalized
38
+ // like deniesGitOrEnvPath so case or a backslash cannot alias one. null -> false.
39
+ const MACHINERY_SET = new Set(MACHINERY);
40
+ export function deniesMachineryPath(rel) {
41
+ if (rel == null) return false;
42
+ return normalizeForDenyCheck(rel).split('/').some((seg) => MACHINERY_SET.has(seg));
43
+ }
44
+
34
45
  // Containment: resolve rel against root; realpath the nearest EXISTING
35
46
  // ancestor so a symlinked directory cannot smuggle the path outside; reject
36
47
  // a symlink as the final component (lstat). Throws on any violation, returns