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,254 @@
1
+ // THE PRACTICE PROJECT.
2
+ //
3
+ // So this is not a mock and not a card. It is a real project in the real store,
4
+ // with real work items in it, and the whole app is scoped to it for the length
5
+ // of the walk. What tells somebody it is not theirs is a band across the top
6
+ // that never leaves, not the size of it.
7
+ //
8
+ // WHY IT EXISTS AT ALL is David, on the call of 2026-08-23: he opened Agentbox,
9
+ // saw a real task on a real screen and read it as work. His words were that he
10
+ // thought the task was to set up the project and that it was asking him
11
+ // questions to do the setup, and he tried to answer the placeholder text.
12
+ //
13
+ // Everything here is content, no window and no filesystem, so both halves can
14
+ // read it: the renderer draws the walk off ../renderer/src/onboarding.ts and
15
+ // the main process writes the rows off here.
16
+
17
+ /**
18
+ * What the practice project is called, on its own row in the rail and in the
19
+ * compose card. The slug is derived from it the same way any project's is. */
20
+ import { NAME } from '../shared/product-name.mjs';
21
+ export const PRACTICE_NAME = 'Practice';
22
+ export const PRACTICE_SLUG = 'practice';
23
+
24
+ /**
25
+ * The mark in project.json. `listProducts` reads it out so the rest of the app
26
+ * can tell a practice project from a real one without matching on a name
27
+ * somebody could give a real project of their own. */
28
+ export const PRACTICE_FLAG = 'practice';
29
+
30
+ /**
31
+ * THE ONE TASK THEY SEND THEMSELVES. Written for them, because the beat is
32
+ * about pressing ⌘↵ and not about thinking of something to ask.
33
+ *
34
+ * AND IT NAMES THE PRETEND APP, WHICH IS THE WHOLE FIX. It read `Add a sign-out
35
+ * button to the header.` over `This one is written for you. Both halves failed
36
+ * her at once. The title is an imperative sentence with no subject and no app
37
+ * in it, so the only app on the screen — the one she is looking at — is the one
38
+ * it appears to be about; and the reassurance underneath is about the CARD
39
+ * rather than about the sentence above it, so it reads as being about some
40
+ * other thing entirely.
41
+ *
42
+ * IT STAYS A REQUEST, AND THAT IS DELIBERATE. It was written in the past tense
43
+ * for one build, to match the four rows already waiting in the practice
44
+ * inbox, and it broke the beat it belongs to. THIS IS THE ONE ROW SHE SENDS:
45
+ * she reads it, presses ⌘↵ on a button that says `Start it`, and two seconds
46
+ * later `PRACTICE_ANSWER` comes back saying `Done, and the tests pass.` A
47
+ * title reading `Added a sign-out button` sits above a Start button asking her
48
+ * to start something the card says is already finished, and is then answered
49
+ * `Done`; the three sentences stop making sense as a sequence. The other four
50
+ * rows are past tense because they are already answered when she meets them.
51
+ * This one is a request because a request is what it is, and every real work
52
+ * item in Agentbox is named that way.
53
+ *
54
+ * SO THE APP'S NAME IS THE FIX, NOT THE TENSE. `Practice` is the pretend project — the name in the
55
+ * rail, in the band across the top, in the compose card's own "For Practice."
56
+ * clause, and in the sidebar note's `practice.local:4000`. There is no second
57
+ * pretend name to invent; the one the walk already says everywhere is the one
58
+ * that goes in the sentence. The body then says the two things that kill her
59
+ * reading outright: it is not her code, and it is not Agentbox.
60
+ *
61
+ * BOTH STRINGS SHOW AT ONCE. `Compose` types the title into the box and then
62
+ * appends a blank line and the body (renderer/src/components/Compose.tsx), so
63
+ * what she reads at beat ten is these two paragraphs together, under the
64
+ * practice band and over a "Start it ⌘↵" button. They have to make sense as
65
+ * one card, which is why the body opens by naming the app the title names. */
66
+ export const PRACTICE_TASK = {
67
+ title: 'Add a sign-out button to the Practice app.',
68
+ body: `Practice is a pretend project, not your code and not ${NAME}. This one is written for you already.`,
69
+ };
70
+
71
+ /**
72
+ * AND WHAT COMES BACK. The old walk read a readme out of the folder they chose
73
+ * (main/first-run.mjs) because the task ran against their own project. This
74
+ * one runs against a project that is not real, so the answer is written down
75
+ * here: it is a decision an agent made on its own and told them about, which
76
+ * is the shape the next beat teaches them to close.
77
+ *
78
+ * IT STILL TAKES TWO SECONDS AND IT IS STILL A REAL ROW WITH A REAL RESULT ON
79
+ * THE REAL LEDGER.*/
80
+ export const PRACTICE_ANSWER =
81
+ 'Done, and the tests pass. I put it next to the avatar rather than inside the menu. Say the word and I will move it.';
82
+
83
+ /**
84
+ * WHAT IS IN THE PRACTICE PROJECT'S SIDEBAR NOTE.
85
+ *
86
+ * AND IT IS NOT A GOALS FORM, which is what the first cut of it turned into. The
87
+ * sentence above is her saying what SHE keeps in hers, as an example; the note
88
+ * that shipped had two headings, **Goals this week** and **Next steps**, and
89
+ * nothing else, which reads as the two things this panel is for. So the sample
90
+ * shows a MIX now — a decision, a number, a link, one thing to do next —
91
+ * because the lesson is that anything can go here, and a sample of one kind
92
+ * teaches the opposite.
93
+ *
94
+ * So the practice project's note is not a placeholder and not a lorem line. It
95
+ * is what a founder's own note looks like, and it is on the screen for every
96
+ * beat of the practice, because the panel it draws in is per project and the
97
+ * whole app is scoped to this one while the walk is on.
98
+ *
99
+ * IT IS THE SAME TEXT IN BOTH PLACES. `createPractice` writes it to the
100
+ * project's `pinned.md`, which is the real file the sidebar reads
101
+ * (main/rail-note.mjs), and the third introduction slab draws it beside the
102
+ * words. A note the introduction promises and the practice project then does
103
+ * not have is the sort of quiet lie the walk has been caught in twice.
104
+ *
105
+ * SHORT ON PURPOSE. The panel is 322px wide with 12px of padding either side,
106
+ * so a line much past forty characters wraps, and a wrapped goal reads as a
107
+ * paragraph rather than a goal. */
108
+ export const PRACTICE_NOTE = [
109
+ 'Sessions are cookies, not tokens.',
110
+ '',
111
+ '15 new users this week. At 6.',
112
+ '',
113
+ 'Staging: practice.local:4000',
114
+ '',
115
+ 'Next: talk to the five who churned.',
116
+ ].join('\n');
117
+
118
+ /* * * THE FOUR WAITING ROWS, and processing them is the point of beats thirteen, * fourteen
119
+ and fifteen. * * EACH ONE IS A WHOLE CONVERSATION NOW, NOT A TITLE AND A LINE.What she had
120
+ * opened was beat twelve, where the reading pane said "Nothing has been said * here yet."
121
+ under the title. These rows were a title, a one-line result and * nothing else, so the one
122
+ surface in the whole walk that shows what Agentbox * actually holds was blank. * * So every
123
+ row carries three things: `body`, the sentence a person typed to * start it; `trace`, the
124
+ lines the run wrote while it worked, in the exact * shape `traceStreamLine` in
125
+ main/supervisor.mjs writes a real one; and * `result`, the finished word. The store writes
126
+ all three (`stagePracticeRows` * and the trace beside it), and the reading pane draws them
127
+ with the same * components it draws a real run with. NOTHING IS MOCKED AND NO COMPONENT *
128
+ KNOWS THESE ROWS EXIST. * * THE TRACE LINE FORMAT IS NOT DECORATION. `HH:MM:SS [Tool]
129
+ argument` is what * the supervisor writes and what renderer/src/item-thread.ts parses back
130
+ into * the quiet work lines of a thread; a line that is not a tool call is the * agent
131
+ talking. Get the two spaces wrong and the thread draws the timestamp * as part of the
132
+ sentence. * * THEY NO LONGER SAY "Example" IN THEIR OWN TITLES, and that is a deliberate *
133
+ change with her older word behind it. These sit in a project called Practice, * under a
134
+ band reading "nothing in here is saved", in a walk that said the same * thing on a screen
135
+ of its own two beats earlier. The band is the stronger * version of her fix, and it is on
136
+ screen the whole time. The old copy is kept * in decisions.md, 2026-08-23. * * AND EACH ONE
137
+ IS A DIFFERENT SHAPE, WITH A DIFFERENT RIGHT MOVE ON IT. The * flags are fields here rather
138
+ than guesses made from the kind, because which * row is which IS the lesson and it must not
139
+ be inferable wrongly: * * two are FINISHED, and E is right on them (`clear`) * one is
140
+ `later`, real work that is nobody's emergency (`snooze`) * one is `waiting`, an agent
141
+ stopped on a question (`unblock`) * * Closing the last two is the one move the product
142
+ exists to stop somebody * making, which is why they get beats of their own rather than a
143
+ fourth press * of E.
144
+ */
145
+ export const PRACTICE_ROWS = [
146
+ {
147
+ kind: 'task',
148
+ title: 'Added the sign-in route. Tests green.',
149
+ body: 'Add a sign-in route at /sign-in. Reuse whatever we already have for sessions.',
150
+ trace: [
151
+ 'Reading how auth works here before I touch anything.',
152
+ '[Read] src/server/auth.ts',
153
+ '[Read] src/server/routes.ts',
154
+ '[Grep] createSession',
155
+ 'There is already a signed session cookie in here, seven days, refreshed on use. I will reuse it rather than adding tokens, because two ways to be signed in is the bug that follows.',
156
+ '[Write] src/server/routes/sign-in.ts',
157
+ '[Edit] src/server/routes.ts',
158
+ '[Edit] src/server/auth.ts',
159
+ '[Bash] npm test -- auth',
160
+ 'Fourteen tests, all green, and nothing else in the suite moved.',
161
+ ],
162
+ result: 'I reused the session cookie rather than tokens. Say if you disagree.',
163
+ agoMs: 6 * 60_000,
164
+ },
165
+ {
166
+ kind: 'task',
167
+ title: 'Fixed the flaky checkout test.',
168
+ body: 'checkout.spec.ts fails about one run in five on CI and passes every time on my Mac. Find out why.',
169
+ trace: [
170
+ '[Bash] npx vitest run checkout.spec.ts --repeat 20',
171
+ 'Three failures in twenty, every one of them on the same assertion, so it is a race and not the assertion.',
172
+ '[Read] tests/checkout.spec.ts',
173
+ '[Read] tests/fixtures/cart.ts',
174
+ 'The fixture writes the cart and returns before the write lands. The test reads it straight away, so on a slow machine it reads an empty cart.',
175
+ '[Edit] tests/fixtures/cart.ts',
176
+ '[Bash] npx vitest run checkout.spec.ts --repeat 40',
177
+ 'Forty for forty. I changed the fixture and nothing in the test itself.',
178
+ ],
179
+ result: 'It was a race on the fixture. It awaits now. Nothing else changed.',
180
+ agoMs: 14 * 60_000,
181
+ },
182
+ /* * THE ONE THAT IS NOT FOR TODAY, and snoozing it is the point of beat fourteen.
183
+
184
+ IT HAD TO BE A FOURTH SHAPE OF ROW, not one of the three already here. Two
185
+ of those are finished, so E is right on them, and the third is an agent
186
+ stopped, where answering is the only right move. Snoozing either would be
187
+ teaching the key on a row it is wrong for, which is the exact fault round
188
+ four was opened about. This one is real work, it is nobody's emergency, and
189
+ half a day is the reason to put it off rather than an excuse: that is the
190
+ row S exists for.
191
+ */
192
+ {
193
+ kind: 'task',
194
+ later: true,
195
+ title: 'The check runs are 40 seconds slower than Monday.',
196
+ body: 'CI got slower this week and nobody changed the workflow. Find out what did it.',
197
+ trace: [
198
+ '[Bash] gh run list --limit 40 --json databaseId,createdAt,conclusion',
199
+ 'Monday it was 2m14s and today it is 2m54s, and the step that grew is the type check rather than the tests.',
200
+ '[Read] .github/workflows/ci.yml',
201
+ '[Read] tsconfig.json',
202
+ '[Bash] npx tsc --noEmit --extendedDiagnostics',
203
+ 'The type check now runs over every package instead of the ones that changed, because the project references came out of tsconfig in the pnpm move.',
204
+ 'Putting them back means splitting the config per package and re-pointing the workflow. It is about half a day and nothing is broken while it waits.',
205
+ ],
206
+ result: 'It is the type check running over every package. The fix is half a day of config, and nothing breaks while it waits.',
207
+ agoMs: 22 * 60_000,
208
+ },
209
+ /*
210
+ * THE ONE THAT IS WAITING. It is a question, it is not finished, and an agent
211
+ is stopped on it until somebody answers. Its result carries the options,
212
+ because `optionsFrom` in renderer/src/format.ts reads the offer off the
213
+ result first, and the options are what make answering it one key. */
214
+ {
215
+ kind: 'question',
216
+ waiting: true,
217
+ title: 'Delete 340 lines of dead code?',
218
+ body: 'Have a look for anything in src/ that nothing imports.',
219
+ trace: [
220
+ '[Bash] npx knip --include files,exports',
221
+ '[Read] src/lib/legacy-client.ts',
222
+ '[Grep] legacy-client',
223
+ '[Read] src/lib/format-old.ts',
224
+ '[Grep] formatOld',
225
+ 'Three files, 340 lines. Nothing in src/ imports any of them and nothing in tests/ does either.',
226
+ 'The legacy client is the one I would ask about: it is the only thing left that speaks to the v1 API, so if anything outside this repository still calls that, deleting it is the end of it.',
227
+ ],
228
+ result: [
229
+ 'Nothing imports any of the three, so this is safe on my side, but the legacy client is the last thing that speaks v1 and I would rather you said so.',
230
+ '',
231
+ '## Options',
232
+ '1. Delete all three (recommended)',
233
+ '2. Keep the legacy client, delete the other two',
234
+ '3. Leave all of it alone',
235
+ ].join('\n'),
236
+ agoMs: 31 * 60_000,
237
+ },
238
+ ];
239
+
240
+ /**
241
+ * THE WALK'S OWN TASK, AS A CONVERSATION. The person sends it at beat nine and
242
+ * opens it at beat eleven, and until 2026-08-24 what they opened was the words
243
+ * "Nothing has been said here yet." under the title: the row had a result and
244
+ * no thread at all. This is the run behind `PRACTICE_ANSWER`, in the same
245
+ * shape as the traces above, so the first task anybody opens in Agentbox shows
246
+ * them what a task holds. */
247
+ export const PRACTICE_TASK_TRACE = [
248
+ '[Read] src/components/Header.tsx',
249
+ '[Grep] useSession',
250
+ 'The header already knows who is signed in, so this is one button and one call.',
251
+ '[Edit] src/components/Header.tsx',
252
+ '[Bash] npm test -- header',
253
+ 'Green. I put it beside the avatar rather than inside the menu, because the menu is three clicks deep on mobile.',
254
+ ];
@@ -0,0 +1,100 @@
1
+ // THE PRE-WRITTEN ANSWER.
2
+ //
3
+ // SHE WAS RIGHT, AND IT IS MEASURED. Run over the 29 project folders in
4
+ // ~/Desktop/dev on 2026-08-21 that gave a sentence saying what the project is
5
+ // for 16 of them. A first task that is wrong or empty on nearly half of the
6
+ // folders somebody might point at is not a first task.
7
+ //
8
+ // SO THE SENTENCE IS OURS AND THE NUMBERS ARE THEIRS. That is exactly what she
9
+ // asked for: pre-written, and true about whatever project gets chosen. It
10
+ // cannot be wrong about a project because it says nothing about what the
11
+ // project IS. Run over the same 29 folders it is correct on 29, in a median of
12
+ // 7.5ms.
13
+ //
14
+ // WHAT THE README LINE IS STILL FOR. It is a description and the counts are
15
+ // not, so it goes on doing the job it was wanted for, which is the line under
16
+ // the project's name in the sidebar (hers, 08-20 12:07). When a folder has no
17
+ // description written down the sidebar simply has no line, which is what it
18
+ // does today for every project.
19
+ //
20
+ // Pure on purpose: the reading of the disk is in main/first-run.mjs.
21
+
22
+ /**
23
+ * How deep the count goes. Her bound from round four was the readme and the
24
+ * top level; a count of the top level alone says two files about a real
25
+ * project, so this goes three deep and skips what is not hers. */
26
+ import { NAME } from '../shared/product-name.mjs';
27
+ export const DEPTH = 3;
28
+
29
+ /**
30
+ * Never counted: none of it is the project, and node_modules alone would put
31
+ * the number in the tens of thousands and the read into seconds. */
32
+ export const SKIP = new Set([
33
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt', 'target',
34
+ 'venv', '.venv', '__pycache__', '.cache', 'vendor', 'Pods', 'DerivedData',
35
+ ]);
36
+
37
+ /**
38
+ * What a file extension is called out loud. A language nobody here names is
39
+ * simply not mentioned, rather than guessed at from the extension. */
40
+ export const LANGUAGE = {
41
+ '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript', '.mjs': 'JavaScript',
42
+ '.cjs': 'JavaScript', '.jsx': 'JavaScript', '.py': 'Python', '.rs': 'Rust',
43
+ '.go': 'Go', '.swift': 'Swift', '.rb': 'Ruby', '.java': 'Java', '.kt': 'Kotlin',
44
+ '.c': 'C', '.h': 'C', '.cpp': 'C++', '.cs': 'C#', '.php': 'PHP', '.sh': 'shell',
45
+ '.md': 'Markdown', '.html': 'HTML', '.css': 'CSS', '.sql': 'SQL', '.ex': 'Elixir',
46
+ };
47
+
48
+ /**
49
+ * The language most of it is written in, or null when nothing here is code we
50
+ * can name. Never a guess: a folder of things this table has never heard of
51
+ * gets a sentence with no language in it rather than a wrong one. */
52
+ export function mainLanguage(ext) {
53
+ let best = null;
54
+ for (const [e, n] of Object.entries(ext ?? {})) {
55
+ if (!LANGUAGE[e]) continue;
56
+ if (!best || n > best[1]) best = [e, n];
57
+ }
58
+ return best ? LANGUAGE[best[0]] : null;
59
+ }
60
+
61
+ function plural(n, one, many) {
62
+ return `${n.toLocaleString('en-US')} ${n === 1 ? one : many}`;
63
+ }
64
+
65
+ /**
66
+ * How long ago, in the words a person uses. Only reached when a folder is not
67
+ * a git repo, because a repo's own answer is better. */
68
+ export function howLongAgo(ms, now = Date.now()) {
69
+ const days = (now - ms) / 86_400_000;
70
+ if (days < 1) return 'today';
71
+ if (days < 2) return 'yesterday';
72
+ // Singular where it lands on one, because "1 years ago" is the kind of small
73
+ // wrongness that makes a person stop believing the rest of the sentence.
74
+ if (days < 14) return `${plural(Math.round(days), 'day', 'days')} ago`;
75
+ if (days < 60) return `${plural(Math.round(days / 7), 'week', 'weeks')} ago`;
76
+ if (days < 365) return `${plural(Math.round(days / 30), 'month', 'months')} ago`;
77
+ return `${plural(Math.round(days / 365), 'year', 'years')} ago`;
78
+ }
79
+
80
+ /**
81
+ * THE ANSWER. One sentence, ours, filled in from whatever was counted.
82
+ *
83
+ * `files`, `dirs` and `ext` are the count. `lastCommit` is git's own words
84
+ * ("2 minutes ago") when the folder is a repo, and `newest` is the newest file
85
+ * in it for when it is not. Nothing in here can be false about a project,
86
+ * which is the whole point of it. */
87
+ export function projectSentence({ name, files, dirs, ext, lastCommit, newest }, now = Date.now()) {
88
+ const who = name || 'This project';
89
+ if (!files) {
90
+ return `${who} is an empty folder. Everything you put in it from here shows up in ${NAME}.`;
91
+ }
92
+ const size = dirs
93
+ ? `${who} is ${plural(files, 'file', 'files')} across ${plural(dirs, 'folder', 'folders')}`
94
+ : `${who} is ${plural(files, 'file', 'files')}`;
95
+ const lang = mainLanguage(ext);
96
+ const when = lastCommit
97
+ ? `its last commit was ${lastCommit}`
98
+ : `it was last touched ${howLongAgo(newest ?? now, now)}`;
99
+ return [size, ...(lang ? [`mostly ${lang}`] : []), `and ${when}`].join(', ') + '.';
100
+ }
@@ -0,0 +1,189 @@
1
+ // A THROWAWAY HOME, SO AGENTBOX CAN BE OPENED BY SOMEBODY WHO HAS NEVER OPENED IT.
2
+ //
3
+ // This is the recipe `scripts/fresh-user.mjs` has used since 2026-08-20, lifted
4
+ // out of it so the app itself can use the same one.A downloaded app has no repo
5
+ // beside it and no terminal in front of it, so the script could never be the
6
+ // answer to the first half of that; the app has to be able to do it to itself.
7
+ //
8
+ // WHY A THROWAWAY HOME AND NOT A RESET. Agentbox keeps its state in three places
9
+ // and a packaged app on her Mac inherits two of them: `<appData>/Astral` (the
10
+ // renderer's settings, her theme, her skin, her zoom, her project order),
11
+ // `zero.config.json` beside it (her store root and her keys), and the store
12
+ // itself wherever storeRoot points. Deleting any of them to get a clean run
13
+ // would take her real setup with it, which is not a thing a test may cost. So
14
+ // NOTHING IS DELETED, EVER. A second copy is launched inside a fresh home under
15
+ // /tmp and all three land in there: fresh settings, no config file so it falls
16
+ // to DEFAULTS, and an empty store at `<throwaway>/<Name>`.
17
+ //
18
+ // WHY IT DOES NOT KILL HER AGENTS, which is the whole reason it is usable.
19
+ // `main.mjs` quits on `app.requestSingleInstanceLock`, and that lock is keyed
20
+ // on userData. A throwaway home means a different userData, so the fresh copy
21
+ // takes its own lock and both run side by side. Nothing quits, `before-quit`
22
+ // never fires, `supervisor.killAll` is never reached.
23
+ //
24
+ // CFFIXED_USER_HOME IS THE LOAD-BEARING ONE. `HOME` alone does NOT move
25
+ // `app.getPath('appData')` on macOS — Chromium asks Core Foundation, not the
26
+ // environment — so a launch with only HOME set shares her userData, hits the
27
+ // lock and quits in under half a second, which is the "it crashes immediately"
28
+ // she reported on 2026-08-20. Both variables, always. `freshEnv` is the only
29
+ // place either is written.
30
+
31
+ import fs from 'node:fs';
32
+ import os from 'node:os';
33
+ import path from 'node:path';
34
+ import { envName, envNames, nameSlug } from './product-name.mjs';
35
+
36
+ /**
37
+ * Where every throwaway home lives. One folder so `--clean` and the ⌘K row's
38
+ * own tidy-up have exactly one thing to delete. */
39
+ export const HOMES = path.join(os.tmpdir(), `${nameSlug}-fresh-user`);
40
+
41
+ // A THROWAWAY INSTALL IS NOT A PERSON, AND IT HAS TO SAY SO OUT LOUD.
42
+ //
43
+ // Why it happened. A throwaway home is a throwaway userData, so
44
+ // `readInstallId` finds no file and writes a NEW random one; and `npm run
45
+ // fresh` launches the PACKAGED app, which carries the baked PostHog key. So
46
+ // every test run arrived as a brand new stranger who opened Agentbox once and
47
+ // never came back, which is also exactly what a real user who bounced looks
48
+ // like. Nothing downstream could tell them apart.
49
+ //
50
+ // THE ID IS NOT MADE STICKY, AND THAT IS DELIBERATE. Carrying an install id
51
+ // across a wipe would make that sentence false for everybody, to fix something
52
+ // that only ever happens on her own Mac. So the throwaway home declares itself
53
+ // instead, and a copy inside one sends under ONE fixed id that names itself as
54
+ // not a person.
55
+ export const FRESH_USER_ENV = envName('FRESH_USER');
56
+ export const FRESH_INSTALL_ID = `${nameSlug}-fresh-user`;
57
+
58
+ /**
59
+ * Is this copy of Agentbox running inside a throwaway home? TWO tests, because
60
+ * the answer still has to be right for a launch route nobody has written yet.
61
+ * The mark is what `freshEnv` sets. The path is the fallback, and it is safe
62
+ * because no real person's home is ever under `HOMES`. */
63
+ export function runningAsAFreshUser(env = process.env) {
64
+ if (env?.[FRESH_USER_ENV] === '1') return true;
65
+ const home = env?.CFFIXED_USER_HOME || env?.HOME;
66
+ return !!home && (home === HOMES || home.startsWith(HOMES + path.sep));
67
+ }
68
+
69
+ /**
70
+ * The throwaway homes on disk, oldest first. Sorted by name, which is a clock
71
+ * stamp, so the last one is the newest. */
72
+ export function existingHomes() {
73
+ if (!fs.existsSync(HOMES)) return [];
74
+ return fs
75
+ .readdirSync(HOMES)
76
+ .map((name) => path.join(HOMES, name))
77
+ .filter((dir) => { try { return fs.statSync(dir).isDirectory(); } catch { return false; } })
78
+ .sort();
79
+ }
80
+
81
+ /**
82
+ * Named by the clock so the newest can be picked by sorting, and so two fresh
83
+ * users started a minute apart are told apart on disk. */
84
+ export function makeHome(now = new Date()) {
85
+ const stamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
86
+ const home = path.join(HOMES, stamp);
87
+ fs.mkdirSync(home, { recursive: true });
88
+ return home;
89
+ }
90
+
91
+ /**
92
+ * Symlinks, not copies: nothing is duplicated, and nothing here can write back
93
+ * over her real install that the app could not already write. */
94
+ function link(from, to) {
95
+ try {
96
+ if (!fs.existsSync(from)) return false;
97
+ // A LINK THAT IS ALREADY THERE IS ONLY GOOD IF IT STILL RESOLVES. `lstat`
98
+ // does not follow a symlink, so a DANGLING one — the target moved, or a
99
+ // throwaway home this one was chained off got tidied away — answered "yes,
100
+ // linked" while the file was not reachable. The copy then reported nothing
101
+ // wrong and told whoever opened it "Agentbox could not find Claude Code on
102
+ // this Mac", which is the one screen in the walk that cannot be argued
103
+ // with. Existence is checked THROUGH the link, and a dead one is replaced
104
+ // rather than believed.
105
+ if (fs.lstatSync(to, { throwIfNoEntry: false })) {
106
+ if (fs.existsSync(to)) return true;
107
+ fs.rmSync(to, { force: true });
108
+ }
109
+ fs.mkdirSync(path.dirname(to), { recursive: true });
110
+ fs.symlinkSync(from, to);
111
+ // And the answer is what the fresh copy will really see, not what we just
112
+ // tried to do.
113
+ return fs.existsSync(to);
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // WHAT IS DELIBERATELY LINKED THROUGH, and why it is still an honest test.
120
+ // Exactly two things, both of which a stranger already has before they ever
121
+ // meet Agentbox, and neither of which Agentbox owns:
122
+ //
123
+ // ~/.local/bin/claude Claude Code itself. The product's prerequisite.
124
+ // ~/Library/Keychains where Claude Code's login actually lives.
125
+ //
126
+ // THE KEYCHAIN ONE IS NOT OPTIONAL AND IT IS NOT OBVIOUS. The token is a
127
+ // `Claude Code-credentials` item in the login keychain, and macOS finds that
128
+ // keychain at `$HOME/Library/ Keychains`, so redirecting HOME hides it. Link
129
+ // it and the same command answers "ok".
130
+ //
131
+ // `~/.claude` ITSELF IS THE ONE CHOICE. `main/agents.mjs` reads
132
+ // `~/.claude/sessions` and `~/.claude/projects`, so linking it puts the Claude
133
+ // Code sessions already on this Mac into the fresh inbox. It is not starting
134
+ // from absolute zero, so it is a flag rather than a default here and the
135
+ // caller says which test it wants.
136
+ export function prepareHome(home, { withAgents = false, realHome = os.homedir() } = {}) {
137
+ const claudeBin = link(path.join(realHome, '.local/bin/claude'), path.join(home, '.local/bin/claude'));
138
+ const keychain = link(path.join(realHome, 'Library/Keychains'), path.join(home, 'Library/Keychains'));
139
+ const agents = withAgents
140
+ ? link(path.join(realHome, '.claude'), path.join(home, '.claude'))
141
+ : false;
142
+ return { home, claudeBin, keychain, agents };
143
+ }
144
+
145
+ /**
146
+ * THE VARIABLES THAT POINT AT HER REAL WORLD, AND WHY A FRESH COPY MAY NOT
147
+ * INHERIT ONE OF THEM.
148
+ *
149
+ * This was the whole bug. `main.mjs` writes the store root into its own
150
+ * `process.env` at startup so every store module can read it at call time, and
151
+ * `freshEnv` spread that environment straight into the child. So the copy that
152
+ * was supposed to be a stranger got a throwaway HOME, no config file and
153
+ * DEFAULTS, exactly as the note at the top of this file promises, and then read
154
+ * the store root out of the environment anyway and opened HER INBOX. She sent a
155
+ * screenshot of it on 2026-09-27: the fresh window listing her own rows, down
156
+ * to the personal ones, with no first run in front of it.
157
+ *
158
+ * Nothing about this is new and it is not the rename's doing. One variable was
159
+ * always enough, and the rename only added the names beside it, which is why
160
+ * this strips EVERY spelling rather than the current one.
161
+ *
162
+ * THE FIXTURE SWITCHES GO TOO, for the same reason rather than a second one. A
163
+ * fresh user is an ordinary first launch of the real app; a copy started from a
164
+ * terminal that had `ZERO_FIXTURES` in it would show canned data and a copy
165
+ * with `ZERO_NO_SUPERVISOR` would never start an agent, and in both cases what
166
+ * she is looking at is not what a stranger would see.
167
+ */
168
+ export const NOT_INHERITED = Object.freeze([
169
+ ...envNames('HOME'),
170
+ 'ZERO_FIXTURES',
171
+ 'ZERO_NO_SUPERVISOR',
172
+ ]);
173
+
174
+ /**
175
+ * The environment a fresh copy is launched with. BOTH VARIABLES, ALWAYS: see
176
+ * the note at the top of this file for what happens with only one. The third
177
+ * is the mark, so the copy knows it is a test and does not count as a person;
178
+ * see `runningAsAFreshUser` above. And everything in `NOT_INHERITED` is taken
179
+ * out, because a stranger's copy may not be handed her store. */
180
+ export function freshEnv(home, env = process.env) {
181
+ const fresh = { ...env, HOME: home, CFFIXED_USER_HOME: home, [FRESH_USER_ENV]: '1' };
182
+ for (const key of NOT_INHERITED) delete fresh[key];
183
+ return fresh;
184
+ }
185
+
186
+ /** Make one and link its two prerequisites, in a single call. */
187
+ export function newFreshHome({ withAgents = false, realHome = os.homedir(), now } = {}) {
188
+ return prepareHome(makeHome(now), { withAgents, realHome });
189
+ }
@@ -0,0 +1,22 @@
1
+ // Claude emits hook failures separately from the result. A nonblocking hook
2
+ // can fail while the model succeeds, so keep its diagnostic without changing
3
+ // delivery/retry state. Successful hooks stay quiet.
4
+ export function hookFailure(obj) {
5
+ if (obj?.type === 'attachment' && obj.attachment?.type === 'hook_non_blocking_error') {
6
+ const a = obj.attachment;
7
+ obj = { type: 'system', subtype: 'hook_response', outcome: 'error',
8
+ hook_name: a.hookName, hook_event: a.hookEvent, exit_code: a.exitCode,
9
+ stderr: a.stderr, stdout: a.stdout };
10
+ }
11
+ if (obj?.type !== 'system' || obj.subtype !== 'hook_response') return null;
12
+ const exitCode = Number.isInteger(obj.exit_code) ? obj.exit_code : null;
13
+ if (obj.outcome !== 'error' && !(exitCode !== null && exitCode !== 0)) return null;
14
+ const name = [obj.hook_name, obj.hook_event].find(v => typeof v === 'string' && v.trim()) ?? 'unnamed';
15
+ const detail = [obj.output, obj.stderr, obj.stdout].find(v => typeof v === 'string' && v.trim()) ?? '';
16
+ const cap = 4000;
17
+ const body = detail.length > cap
18
+ ? `${detail.slice(0, cap)}\n[${detail.length - cap} characters omitted]`
19
+ : detail.trim();
20
+ return `Hook ${name} failed${exitCode !== null ? ` (exit ${exitCode})` : ''}.${body ? `\n${body}` : ''}`;
21
+ }
22
+
@@ -0,0 +1,39 @@
1
+ // THE INSTALLATION IDENTIFIER, and there is exactly one of it.
2
+ //
3
+ // `legal/privacy.html` 5.2: a random identifier in a file, whose only purpose is
4
+ // to let counts from one installation read as one installation rather than as
5
+ // many. It is NOT an account, it is not an email, and it is derived from nothing
6
+ // about the person or the machine, so a second copy of this rule written
7
+ // somewhere else is how it quietly becomes a fingerprint.
8
+ //
9
+ // Two callers, one file: `main/crash-report.mjs` (which needs it while the
10
+ // process is dying) and `main/analytics.mjs`. Both read the same id, which is
11
+ // what makes a crash and the counts around it one story.
12
+
13
+ // ONE EXCEPTION, AND IT IS HER OWN TESTING. A copy launched into a throwaway
14
+ // home by `npm run fresh` or by the ⌘K row is a first-run test, not a person,
15
+ // and before 2026-08-31 each one minted a fresh random id and sent under it.
16
+ // Ten of those reached the brief in a fortnight as strangers who bounced. They
17
+ // send under one fixed id instead. Nothing about a real install changes: a real
18
+ // home is never inside `HOMES`, so the branch below is unreachable on every
19
+ // machine that is not hers mid-test.
20
+ //
21
+ // AND A DEMO COPY IS THE SAME EXCEPTION FOR THE SAME REASON. A demo is opened
22
+ // in front of a room and closed again, which is exactly the shape of a stranger
23
+ // who opened Agentbox once and never came back. It sends under its own fixed id,
24
+ // so a week of demos reads as a week of demos.
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { FRESH_INSTALL_ID, runningAsAFreshUser } from './fresh-user-home.mjs';
28
+ import { DEMO_INSTALL_ID, runningAsADemo } from './demo-world.mjs';
29
+
30
+ export function readInstallId(dir, env = process.env) {
31
+ if (runningAsADemo(env)) return DEMO_INSTALL_ID;
32
+ if (runningAsAFreshUser(env)) return FRESH_INSTALL_ID;
33
+ if (!dir) return null;
34
+ const file = path.join(dir, 'install-id');
35
+ try { return fs.readFileSync(file, 'utf8').trim(); } catch {}
36
+ const id = (globalThis.crypto?.randomUUID?.() ?? String(Math.random()).slice(2));
37
+ try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(file, id); } catch {}
38
+ return id;
39
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "messages": "This is about the LAST message you send, the one that ends your run. Whoever\nasked for it reads it in a list beside twenty others and usually decides what to\ndo next without opening anything. Ten seconds is the real budget.\n\nFIRST, WHAT WERE YOU ASKED FOR. Everything below is written for a run that DID\nsomething and now needs a decision. WHEN YOU WERE ASKED A QUESTION INSTEAD,\nTHE ANSWER IS THE WHOLE DELIVERABLE and these rules serve it rather than\nreplace it: line one carries the answer and never a question back, the length\nis theirs and not ours, and you offer nothing, because work they did not ask\nfor is not a pick. Read this, explain that, what is their methodology,\nsummarise this: those are questions. Answer them the way any good assistant\nwould, then stop.\n\n**Line one is the thing they have to say back, in bold, with nothing above it.**\nYOUR MESSAGE BEGINS WITH THE TWO ASTERISKS. Not a heading and not a sentence of\ncontext first. A question they can answer or a word they can pick. When they\nasked YOU a question, line one is instead the most useful sentence of your\nanswer, still bold and still first, because the thing they have to say back is\nnothing and the thing they came for is the answer. It keeps the same budget\nbelow: if the answer will not fit, the shortest true version of it goes first\nand the rest follows underneath.\nOne sentence, under fifteen words and under 110 characters. The app draws it\ntwice, in the list and as the heading over your options, and both cut it there.\nThe asterisks are where the cut lands: leave them out and it takes your whole\nfirst paragraph and cuts that mid-word. No jargon and no file names.\nNot the topic, not a status, not the title again. If there is genuinely nothing\nto say back, it says what happened instead: \"Done, the tests are green and\nnothing needs you.\"\n\nThe sentence you will want to put above the bold line is housekeeping: the tests\nare green, nothing is committed. It is true, it is not what they have to say\nback, and it goes underneath.\n\nThen the answer itself, in plain full sentences, the way you would say it out\nloud. What happened, what you measured, what you would do next. One idea per\nsentence. Under 200 words, no headings and no bullet lists. Everything you did is\nalready in the conversation above this message, so do not say it twice: this is\nthe ten second version and the detail is one scroll up.\n\nTHE ANSWER IS WRITTEN ONCE, AND THIS MESSAGE IS IT. On a work item the app\nwrites your last message onto the row when you end, options and all. So never\nwrite the answer onto the row yourself as well: that second copy is the answer\ntwice on one screen.\n\nTHAT CAP IS ON REPORTING YOUR OWN WORK, NOT ON ANSWERING THEM. It is short only\nbecause the work is readable above it. An answer they asked for is readable\nnowhere else, so there the ask sets the size, \"give me the details\" means give\nthe details, and headings or a list are fine if that is how the thing is shaped.\nBeing brief about your run is a courtesy. Being brief about their question is\nfailing to answer it.\n\nNever give a number you did not measure. \"I don't know\" is an answer.\n\nTHE ANSWER IS IN THIS MESSAGE, NOT IN A FILE BESIDE IT.Write the finding, the\nnumbers and the recommendation here; make no report to hold them. What you\nwould have made anyway, a design, an app running at a port, a change to\nreview, gets its path or address on its own line, and a design or a change\nopens beside the message by itself. Not the source files you edited, which are\nno use to them.\n\nSteps they must take are a numbered list, five at most, the first one doable now.\n\nIf anything at all is left for them to decide, end with an Options section, in\nthese words. Left for THEM to decide: something already in front of them that\nyour run put there.\n\n## Options\n1. The move you would make (recommended)\n2. A real alternative\n\nLINE ONE AND THE OPTIONS ARE ONE QUESTION, NOT TWO. The options are the answers\nto line one, in the same words. So write line one LAST, off the options you\nended up with, and read them back together: if someone said option one\nout loud, it has to answer line one. When it does not, they have two questions\nstacked on each other and must open the message to see which is which.\n\nTwo options is a fine list and three is the most that helps. Mark exactly one\nrecommended. Every option has to be something you would start on without asking\nanother question first. Name each choice in your prose too, because the list is\noften drawn elsewhere on the screen and a sentence pointing at \"option two\"\nthen points at nothing.\n\nIF YOUR RUN CHANGED ANYTHING, THERE IS SOMETHING TO DECIDE. Edited files, a\nbranch, a draft, a page you built: none of it is finished until the person\nreading you says what happens to it. So the options are what to do with the\nthing, in their words: ship it, change this part first, throw it away. Do not\ncall the run settled to save yourself the asking, or make them type \"merge it\"\nbecause you did not offer it. The only run that ends with no options\nis one that changed nothing.\n\nAND READING CHANGES NOTHING. Reading, searching, explaining and answering\nleave the world as they found it, so a question answered ends with no options,\nand that is the right ending and not a lazy one. NEVER INVENT A PICK OUT OF\nWORK THEY DID NOT ASK FOR. Offering to go and draft something else is not an\noption, it is a new task, and it costs them the answer they came for.\n",
3
+ "adhd": "- The first line is the thing to do or answer, in bold.\n- Steps are a numbered list of five or fewer, and the first one is small.\n- No recap, no preamble, no closing offer.\n- Work in progress says where it is in one clause.\n- Time is given in units, or not at all.\n",
4
+ "system": "# How to work\n\n<!-- store-tools:start -->\nYou are a worker session in {{name}}, the founder's agent inbox. You were spawned\nfor exactly one work item (above), you do that work, you write your state back,\nand you end. There is no long-lived you: anything you do not write into the\nwork item or the product's documents is gone when you exit. Sessions are\ncattle; the store is the truth.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nYou are a worker session in {{name}}, the founder's agent inbox. You were spawned\nfor exactly one work item (above), you do that work, and you end. There is no\nlong-lived you.\n\nTHIS INSTALL GIVES YOU NO TOOLS FOR THE INBOX ITSELF. You cannot claim this\nitem, check in on it, file another one, set its status or close it, and nothing\nyou attempt that way will work. Do not go looking for those tools and do not\nhand-edit the inbox's own files to fake them.\n\nTwo things of yours reach the founder, and only two. The plain sentences you say\nas you go, which the app puts on the row while you are working. And YOUR LAST\nMESSAGE, which the app writes onto the row as the result the second you exit,\nand which parks the row in her inbox for her to read and reply to. So your last\nmessage is not a sign-off. It is the whole of what she gets, and the rules for\nwriting it are at the end of this brief.\n<!-- no-store-tools:end -->\n\nThe founder is building several products at once through this inbox. Their\nattention is the scarcest resource in the system, scarcer than your tokens.\nEverything below follows from that.\n\n## The store\n\n<!-- store-tools:start -->\nThe product's docs directory holds its documents and work items; use the store\ntools, not the files. Make a document only when plain Claude Code would; the\nanswer is the message.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nThe product's docs directory holds its documents; edit them with your ordinary\ntools, and make one only when plain Claude Code would. Its work items are NOT\nyours to edit: the app owns that file, and your last message is how this row\ngets written.\n<!-- no-store-tools:end -->\n\nThe product's documents are your context; read what you need before acting.\nA decision recorded there holds unless the founder has said otherwise since.\n\n## Altitude\n\n<!-- store-tools:start -->\nA founder directive is not always a product task. It can be spoken at the\nlevel of the work system itself: priorities, what to scrap, how to operate\n(close everything that does not serve X, stop pursuing Y). Execute those\ndirectly: close the obsolete items yourself with a one-line reason each, file\none review summarizing what you closed and kept and why, and treat the stated\npriorities as the newest grounding for everything after.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nA founder directive is not always a product task. It can be spoken at the\nlevel of the work system itself: priorities, what to scrap, how to operate\n(close everything that does not serve X, stop pursuing Y). Treat those as the\nnewest grounding for everything after. You cannot close anybody's rows, so\nname the ones you believe are dead in your last message, one line of reason\neach, and let her clear them.\n<!-- no-store-tools:end -->\n\n## The ask sets the size of the run\n\nEVERYTHING BELOW DESCRIBES THE BIGGEST KIND OF RUN, and most rows are not\nthat. Its length is not an instruction to make your row bigger than it was. A\nrow that asks a question wants an answer, not a project: read it, answer it,\nstop.\n\nSo on anything general, read this PDF, what does this code do, find me X, you\nare a good general assistant first and this brief is scaffolding around that.\nIf following a rule here would make your answer worse than a plain Claude Code\nsession's, you are misapplying it. Answer well, then apply what actually fits.\n\n## Doing the work\n\n<!-- store-tools:start -->\n- Claim your item first (claim_work_item with your item id). If the claim is\n refused, someone else has it: end immediately, reporting nothing.\n- Leave priority alone on anything you file. The app decides what runs next\n from the founder's own order over her products, and her tag on an item is\n the only one that changes it.\n- Actions outside your standing grants do not silently fail any more: the\n permission system asks the founder LIVE (an approval card in her inbox app)\n and blocks you until she answers, up to 15 minutes. Spend these asks like\n interruptions, because they are: exhaust what your grants already allow,\n batch the gated part into as few commands as possible, and if she denies\n or the ask times out, do not retry it; finish what you can and file the\n remainder as a review with the exact commands.\n- EVERYTHING YOU FILE IS A PROPOSAL: nothing you create runs until the founder\n approves it from the inbox, so write it for her to decide rather than as a\n note to another agent.\n- Code changes happen in the product's repo (your cwd), on a branch, committed\n as you go. Name the branch the way this repo already names branches; read a\n few with `git branch` and follow them. Never commit to main; the founder\n reviews through the inbox.\n- YOUR CWD IS ALREADY YOUR OWN FOLDER, one per task on its own branch. Do not\n make a second one or delete this one. Your branch keeps the commits.\n- CHANGE A FILE WITH THE EDITING TOOL, NEVER WITH sed OR A HEREDOC. Your own\n harness instructions tell you the opposite and they are wrong here, so this\n line overrules them. Two reasons, and the second one is the one that gets\n forgotten. The card the founder and her users open to read what you changed\n now reads the repository as well, so a file you rewrite with a shell command\n does reach it, but ONLY if your work is in a checkout: on a product with no\n repository the conversation is the only reading there is. And the sentence\n you wrote just before a change, which is what makes the change readable by\n someone who is not you, exists nowhere else at all. Reading and searching\n with the shell stays right and stays faster. Writing does not.\n- NAME EACH STEP IN A SENTENCE SHE WOULD USE, out loud, before you take it.\n One plain line, no jargon and no tool names: \"Now the IPC handlers and the\n bridge\", not \"Running Edit on main/ipc.mjs\". Those sentences are what the\n founder actually reads about your run: the app shows the newest one while you\n work and keeps the rest as the record of what happened, with everything you\n typed folded away behind them. A session that narrates nothing leaves a row\n that says it did nothing.\n- When the work is done, set the status if it needs setting, then END WITH YOUR\n ANSWER AS YOUR LAST MESSAGE: what changed and how you verified it, shaped by\n the rules at the foot of this brief. The app writes that message onto the row\n as its result the moment you exit. Do not pass `result` for this row yourself;\n the answer said twice is the fault this replaces.\n- ANSWERING HER IS NOT CLOSING HER ROW. Hers, 2026-09-27: \"If I ask for\n something, its job is to return it back. Only when I request it, 'Close it,'\n or say, 'All right, I'm done with this,' or something like that, should it\n close it.\" So on a row she wrote (labelled `founder`), answer and leave the\n status alone: the row stays open and reaches her as an answer. Set\n it done only when she has asked you to, and then pass her exact words asking\n for it as `closeBecause`; the store leaves the row open without them. Rows an\n agent filed are still yours to close, except that a thread she is talking on\n is hers to end: if her reply was a request for information, or the work it\n points at is still ahead of you, leave it open or blocked.\n- If you set status blocked, say what would unblock it, and when that is the\n founder's to do, file it as a child work item (parent: this item id) so her\n row can take her straight to it.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\n- There is nothing to claim and nothing to release. The app gave this row to\n you alone before it started you, and it takes it back when you exit.\n- Actions outside your standing grants do not silently fail any more: the\n permission system asks the founder LIVE (an approval card in her inbox app)\n and blocks you until she answers, up to 15 minutes. Spend these asks like\n interruptions, because they are: exhaust what your grants already allow,\n batch the gated part into as few commands as possible, and if she denies\n or the ask times out, do not retry it; finish what you can and say what is\n left in your last message, with the exact commands.\n- You cannot file work, so nothing you find becomes a task by itself. Anything\n worth doing next goes in your last message, in one sentence, as something she\n can say yes to. Her attention is the gate here on purpose.\n- Code changes happen in the product's repo (your cwd), on a branch, committed\n as you go. Name the branch the way this repo already names branches; read a\n few with `git branch` and follow them. Never commit to main; the founder\n reviews through the inbox.\n- YOUR CWD IS ALREADY YOUR OWN FOLDER, one per task on its own branch. Do not\n make a second one or delete this one. Your branch keeps the commits.\n- NAME EACH STEP IN A SENTENCE SHE WOULD USE, out loud, before you take it.\n One plain line, no jargon and no tool names: \"Now the IPC handlers and the\n bridge\", not \"Running Edit on main/ipc.mjs\". Those sentences are what the\n founder actually reads about your run: the app shows the newest one while you\n work and keeps the rest as the record of what happened, with everything you\n typed folded away behind them. A session that narrates nothing leaves a row\n that says it did nothing. It is also the only progress she can see before you\n finish, so say one before every real step.\n- When the work is done, STOP AND WRITE THE LAST MESSAGE. That message is the\n result: what changed and how you verified it, in two or three sentences,\n shaped by the rules at the foot of this brief. The app lands it on the row and\n puts the row in her inbox. There is no other way to report and no second\n chance at it.\n- If something stopped you, the first line of your last message says what\n stopped you and what would undo it. The row reaches her either way, so a\n blocker she can act on is worth more than a partial job dressed up as a\n finished one. Her reply comes back to a fresh session on this same row, with\n everything said here in front of it.\n<!-- no-store-tools:end -->\n\n<!-- subagent-rule:start -->\n## One row, or several\n\nSplit work into separate work items when each piece needs its own answer from\nthe founder or its own session. Keep helpers inside one item when their work\ncombines into a single result.\n\nTwenty issues off one call are twenty rows. A research task fanned across ten\nsubagents that merge into one document is one row, however many subagents it\ntakes. Spawning a subagent is not by itself a reason to file anything.\n<!-- subagent-rule:end -->\n\n## Honesty\n\nReport what actually happened. A test you did not run is not \"passing\"; a\nthing you could not do is stated as such, with what you tried. A fabricated\nresult in this system poisons the founder's view of their own company, which\nis the worst failure available to you.\n\n<!-- repeat-rules:start -->\n## If this item is one run of a repeating task\n\nIts body says so at the top, and its labels carry `repeat:<id>`. It runs again\ntomorrow whatever happens here, so this run does not have to carry the future:\nfinish the day's work, not the whole standing job.\n\n<!-- store-tools:start -->\nFinish one of two ways.\n\nNothing she needs to know: update the item with `status: \"done\"` and labels\n`[\"founder\", \"repeat:<id>\", \"clean\"]` IN THE SAME CALL, sending all three\nbecause labels replace the array rather than merging, and end on one short\nline. That is what clean means. Finishing quietly is the ordinary case for a\ntask that runs every morning, and it is why she can leave one running.\n\nAnything she should see: finish normally, with a last message saying what you found,\nand do NOT set the clean label. It reaches her inbox, which is the entire point\nof running this daily. Anything that deserves its own life gets filed as its\nown item, the ordinary way.\n\nThe clean label is the only thing in {{name}} that makes finishing hide a row,\nso it is required to hide and never to show. If you are unsure which of the\ntwo this is, it is the second one.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nThe clean label is the only thing in {{name}} that makes finishing hide a row,\nand setting a label is one of the things you cannot do here. So there is no\nquiet finish available to you: your last message reaches her every morning\nwhatever it says. On a run with nothing in it, say that in one line and stop.\nOne line costs her a second to read and tells her the job is still alive.\n<!-- no-store-tools:end -->\n<!-- repeat-rules:end -->\n"
5
+ }