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,146 @@
1
+ // The founder's running order over her companies, and the one scoring rule
2
+ // derived from it.
3
+ //
4
+ // This file exists because the rule was written twice: once in the supervisor
5
+ // (which decides what to spawn) and once in the renderer (which decides what
6
+ // the inbox shows first). Two copies of "what matters most" is two answers to
7
+ // the same question, and the day they disagree is the day the top of the inbox
8
+ // stops being what the agents are actually working on. There is one copy now,
9
+ // imported by both.
10
+ //
11
+ // The model is an ORDER, not a set of buckets (founder's call, 2026-08-06).
12
+ // Buckets leave every tie inside a bucket unanswered, and "which of these two
13
+ // comes first" is the question the fleet needs settled.
14
+
15
+ // A place in the order is worth this much, so it dominates any item's own
16
+ // priority (which runs 0..9). A project one step higher in the order beats
17
+ // anything filed under a project one step lower, whatever its tag says.
18
+ export const RANK_STEP = 100;
19
+
20
+ /** Clean a proposed order: strings only, no blanks, no duplicates. */
21
+ export function normalizeOrder(slugs) {
22
+ if (!Array.isArray(slugs)) return [];
23
+ const seen = new Set();
24
+ const out = [];
25
+ for (const slug of slugs) {
26
+ if (typeof slug !== 'string' || !slug || seen.has(slug)) continue;
27
+ seen.add(slug);
28
+ out.push(slug);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ /**
34
+ * The score contributed by a product's place in the order. Higher is sooner.
35
+ *
36
+ * An UNRANKED product scores zero, which puts it below every ranked one and
37
+ * ties it with the other unranked. That is deliberate: a product the founder
38
+ * has never placed must not silently outrank one she has, and an empty order
39
+ * has to behave exactly like the flat inbox that existed before ranking did.
40
+ */
41
+ export function productRankScore(order, slug) {
42
+ const idx = order.indexOf(slug);
43
+ if (idx === -1) return 0;
44
+ return (order.length - idx) * RANK_STEP;
45
+ }
46
+
47
+ /**
48
+ * The number an item carries into that score.
49
+ *
50
+ * An AGENT'S tag counts for nothing. Agents used to set this themselves, on a
51
+ * rule in the worker brief that told them 7 meant blocking; of the 442 items
52
+ * agents filed, 127 came in at 7 or higher, which is not an order, it is
53
+ * noise. She asked for the ordering to be the app's job, the rule is out of
54
+ * the brief, and this is the half that also covers the items already sitting
55
+ * in the store carrying an agent's 8.
56
+ *
57
+ * Everything else counts, and the test for "hers" is deliberately wide: her
58
+ * compose box writes the number as 'system' rather than 'founder' (the split
59
+ * in store.composeItem is words-versus-lifecycle, not authority), and a repeat
60
+ * rule's number arrives the same way. Reading only 'founder' here would have
61
+ * thrown away the Urgent tag on the very row she complained about
62
+ * (tests/urgent-does-not-wait-behind-medium.test.mjs).
63
+ *
64
+ * Nought means untagged, not bottom. It is the seed the fold starts an item
65
+ * at and the default her compose sends when she picks no number, and her own
66
+ * scale starts at Low, which is 2. An untagged row therefore sits in the
67
+ * middle with everything else and her order over the products, then age,
68
+ * decides it.
69
+ */
70
+ export const DEFAULT_PRIORITY = 5;
71
+
72
+ export function itemPriority(item) {
73
+ if (!item) return DEFAULT_PRIORITY;
74
+ if (item.wrote?.priority?.source === 'agent') return DEFAULT_PRIORITY;
75
+ const n = item.priority;
76
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_PRIORITY;
77
+ }
78
+
79
+ /**
80
+ * The top of her own scale, and the only level that INTERRUPTS.
81
+ *
82
+ * The number is the renderer's (renderer/src/priority.ts: Urgent is 9), and it
83
+ * is named here because the supervisor needs the same answer and importing a
84
+ * .ts module into the main process is not available to it. Same reason the
85
+ * whole file exists: the day the two copies disagree is the day Urgent stops
86
+ * meaning urgent in one half of the app.
87
+ */
88
+ export const URGENT_PRIORITY = 9;
89
+
90
+ /**
91
+ * Is this HER urgent row?
92
+ *
93
+ * itemPriority already throws away an agent's number, so this cannot be
94
+ * reached by a worker tagging its own proposal a nine. That matters more here
95
+ * than anywhere else in this file: everything downstream of this predicate
96
+ * takes a running session away from something else.
97
+ */
98
+ export function isUrgent(item) {
99
+ return itemPriority(item) >= URGENT_PRIORITY;
100
+ }
101
+
102
+ /**
103
+ * Migration from the three-bucket model. Only ever runs against state written
104
+ * before the order existed, and preserves the founder's intent: everything she
105
+ * called high comes first, then untagged, then everything she called low.
106
+ */
107
+ export function orderFromTiers(tiers) {
108
+ if (!tiers || typeof tiers !== 'object') return [];
109
+ const slugs = Object.keys(tiers);
110
+ const rank = (s) => (tiers[s] > 0 ? 0 : tiers[s] < 0 ? 2 : 1);
111
+ return normalizeOrder(slugs.filter((s) => tiers[s] !== 0).sort((a, b) => rank(a) - rank(b)));
112
+ }
113
+
114
+ /**
115
+ * Where a dragged chip would land, in READING ORDER, given the chips' boxes and
116
+ * the pointer. Returns an index to insert BEFORE; the length means "at the end".
117
+ *
118
+ * This is pure geometry and lives here rather than in the component because it
119
+ * is the half of ranking that was wrong. The first version compared the pointer
120
+ * against chip centres on the X axis alone, which is right for one row and
121
+ * useless for four: with twenty two projects the chips WRAP, every row spans
122
+ * the same X range, and dragging up or down moved nothing at all (founder,
123
+ * 2026-08-06). Row first, then position within the row.
124
+ */
125
+ export function landingIndex(rects, x, y) {
126
+ if (!Array.isArray(rects)) return 0;
127
+ for (let i = 0; i < rects.length; i += 1) {
128
+ const r = rects[i];
129
+ if (!r) continue;
130
+ if (y < r.top) return i; // above this row entirely
131
+ if (y <= r.bottom && x < r.left + r.width / 2) return i; // this row, left of centre
132
+ }
133
+ return rects.length;
134
+ }
135
+
136
+ /**
137
+ * Move one product to a new index, returning a fresh order. Products that were
138
+ * never in the order join it on their first move, so dragging an unranked chip
139
+ * is how it gets ranked at all.
140
+ */
141
+ export function moveProduct(order, slug, toIndex) {
142
+ const base = normalizeOrder(order).filter((s) => s !== slug);
143
+ const at = Math.max(0, Math.min(base.length, Math.trunc(toIndex)));
144
+ base.splice(at, 0, slug);
145
+ return base;
146
+ }
@@ -0,0 +1,26 @@
1
+ // What the app says after it has swept up a night's stranded work.
2
+ //
3
+ // The two numbers mean different things and must never blur:
4
+ // resumed — sessions handed back their own context. Nothing for her to do.
5
+ // waiting — rows whose session is gone. Her rule was that these wait for her
6
+ // rather than being restarted, so the line has to say where they
7
+ // are, which is the palette command that already exists.
8
+ //
9
+ // Slots are not mentioned. A row that only ran out of capacity resumes itself
10
+ // on the next tick, and a toast that lasts 2.5 seconds has no room for a
11
+ // number that will be wrong before she has finished reading it.
12
+
13
+ export function recoveryToast(r) {
14
+ if (!r) return null;
15
+ const resumed = Number(r.resumed) || 0;
16
+ const waiting = Number(r.waiting) || 0;
17
+ if (!resumed && !waiting) return null;
18
+ const agents = (n) => (n === 1 ? '1 agent' : `${n} agents`);
19
+ if (!waiting) {
20
+ return `${agents(resumed)} picked up where ${resumed === 1 ? 'it' : 'they'} left off`;
21
+ }
22
+ if (!resumed) {
23
+ return `${agents(waiting)} could not be resumed · waiting for you in ⌘K`;
24
+ }
25
+ return `${agents(resumed)} resumed · ${waiting} waiting for you in ⌘K`;
26
+ }
@@ -0,0 +1,11 @@
1
+ // Types for shared/referenced-files.mjs, which is plain ESM so that main (the
2
+ // spawn path's brief) and the renderer (her attachment row) share one copy of
3
+ // the rule about what counts as a file a message refers to.
4
+
5
+ // The last argument may be `{ dir }`, the product's own folder, which is what
6
+ // lets a path written out in full from the root be read as one of ours.
7
+ export function referencedFiles(
8
+ ...texts: Array<string | null | undefined | { dir?: string | null }>
9
+ ): string[];
10
+ export function fileLabel(path: string): { name: string; where: string };
11
+ export function isImagePath(path: string | null | undefined): boolean;
@@ -0,0 +1,122 @@
1
+ // PURE. Every file a message refers to, in the order it refers to them.
2
+ //
3
+ // The last clause is the design. Nothing here asks a worker to remember to
4
+ // attach anything: the message text IS the manifest, so a path a worker merely
5
+ // mentioned in passing lands at the foot of the message like any other
6
+ // attachment. Three ways a worker names a file, all of them already in use:
7
+ //
8
+ // [the report](reports/week-32.md) a markdown link or image
9
+ // `designs/round-3.html` a path in a code span
10
+ // designs/round-3.html a bare path in prose
11
+ //
12
+ // Absolute paths, urls and anchors are not files of this product's, so they are
13
+ // left alone. The extension list is what decides: a bare word with a dot in it
14
+ // is not a reference, and guessing wider would put "e.g" and "v1.2" in her
15
+ // attachment row.
16
+ //
17
+ // IT LIVES IN shared/ BECAUSE THE BRIEF NEEDS IT TOO. This was the renderer's
18
+ // own module, which meant the only reader of a message's manifest was the
19
+ // screen. The spawn path had none, so a task carrying her screenshot was handed
20
+ // to a worker as a relative link with nothing anywhere telling it to open one:
21
+ // 155 of the 320 sessions on Agentbox that were handed one never did. Same rule,
22
+ // one copy, both processes; renderer/src/referenced-files.ts re-exports it.
23
+ // `change` is the newest one and it is the whole answer to David's call.A run
24
+ // that touched the repository writes what it changed as a file, the file lands
25
+ // in the row at the foot of the card like every drawing already does, and
26
+ // clicking it opens the change in the pane.
27
+ const EXT = 'png|jpe?g|gif|webp|svg|html?|pdf|md|markdown|csv|txt|json|ya?ml|sql|sh|css|jsx?|tsx?|mjs|cjs|py|rb|go|rs|toml|change';
28
+
29
+ // A path, as it appears in prose or inside a link's parentheses. Deliberately
30
+ // no leading slash and no scheme: this is a path relative to the product, which
31
+ // is the only thing the app can resolve and open.
32
+ // The lookbehind is what keeps a url or an absolute path from being read as a
33
+ // relative one: without it "https://example.com/a.png" contributes
34
+ // "example.com/a.png" and "/etc/hosts.md" contributes "etc/hosts.md", and both
35
+ // would sit in her attachment row pointing at nothing.
36
+ const PATH = new RegExp(String.raw`(?<![\w./:@-])(?:\.\/)?[\w-][\w./-]*\.(?:${EXT})\b`, 'gi');
37
+ const LINK = new RegExp(String.raw`!?\[[^\]\n]*\]\(\s*([^)\s]+?)\s*(?:\s+"[^"]*")?\)`, 'g');
38
+
39
+ // THE WHOLE PATH, from the root, as a worker that pasted one writes it. Kept
40
+ // separate from PATH above because it is read by a different rule: a relative
41
+ // path is a reference on sight, an absolute one only when `dir` says it lands
42
+ // inside this product's own folder (see `take`).
43
+ //
44
+ // She was reading a card whose one document was written out in full,
45
+ // /Users/…/astral/designs//the-first-row.html. Everything the pane can do with
46
+ // a document hangs off this function: the chips at the foot of the message, the
47
+ // page drawn in a frame under it, the document it opens on its own, and the
48
+ // brief a worker is spawned with. All four were silent, because the path never
49
+ // got past the leading slash. Counted over her Agentbox store the same day: 14 of
50
+ // the 224 cards carrying any text name a file in full like this, and 8 of those
51
+ // mentions point at a file in her own project folder that she should have been
52
+ // able to open and could not.
53
+ //
54
+ // The other 10 are /tmp scratch and the app's own source, which is why the
55
+ // leading slash is not simply allowed. Inside her folder it is the document she
56
+ // was told to look at. Outside it, it is our machinery and stays plain text.
57
+ const ABS_PATH = new RegExp(String.raw`(?<![\w.:@/-])/[\w-][\w./-]*\.(?:${EXT})\b`, 'gi');
58
+
59
+ const ABSOLUTE = /^([a-z]+:|\/|#)/i;
60
+
61
+ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
62
+
63
+ /**
64
+ * Every file referenced across some pieces of a message, deduped, in order.
65
+ *
66
+ * Pass `{ dir }` last, the product's own folder, and a path written out in full
67
+ * from the root that lands inside it is read as the relative path it is. Every
68
+ * caller downstream keeps receiving relative paths and nothing else changes.
69
+ * Leave it out and only relative paths count, which is what this did before.
70
+ */
71
+ export function referencedFiles(...texts) {
72
+ const last = texts[texts.length - 1];
73
+ const opts = last && typeof last === 'object' ? texts.pop() : null;
74
+ const dir = String(opts?.dir ?? '').replace(/\/+$/, '');
75
+
76
+ const found = [];
77
+ const seen = new Set();
78
+
79
+ const take = (raw) => {
80
+ if (!raw) return;
81
+ let path = raw.replace(/^\.\//, '').trim();
82
+ // Written out in full. It is one of ours only when it sits inside this
83
+ // product's folder, and then it is kept as the short path everything
84
+ // downstream already understands.
85
+ if (dir && path.startsWith(dir + '/')) path = path.slice(dir.length + 1);
86
+ if (!path || ABSOLUTE.test(path)) return;
87
+ // a link target that is not a file of ours (an id, a query) is not an
88
+ // attachment; the extension is what makes it one
89
+ if (!new RegExp(String.raw`\.(?:${EXT})$`, 'i').test(path)) return;
90
+ const key = path.toLowerCase();
91
+ if (seen.has(key)) return;
92
+ seen.add(key);
93
+ found.push(path);
94
+ };
95
+
96
+ for (const text of texts) {
97
+ if (!text) continue;
98
+ // links first, so a target that also matches the bare-path scan is recorded
99
+ // once, in the order a reader meets it
100
+ const source = String(text);
101
+ const hits = [];
102
+ for (const m of source.matchAll(LINK)) hits.push({ at: m.index ?? 0, path: m[1] });
103
+ for (const m of source.matchAll(PATH)) hits.push({ at: m.index ?? 0, path: m[0] });
104
+ if (dir) for (const m of source.matchAll(ABS_PATH)) hits.push({ at: m.index ?? 0, path: m[0] });
105
+ hits.sort((a, b) => a.at - b.at);
106
+ for (const hit of hits) take(hit.path);
107
+ }
108
+
109
+ return found;
110
+ }
111
+
112
+ /** What a chip says: the file's own name, with its folder as the quiet part. */
113
+ export function fileLabel(path) {
114
+ const parts = path.split('/');
115
+ const name = parts.pop() ?? path;
116
+ return { name, where: parts.join('/') };
117
+ }
118
+
119
+ /** Whether a referenced path is a picture, which is what a worker must OPEN. */
120
+ export function isImagePath(path) {
121
+ return IMAGE.test(String(path ?? ''));
122
+ }
@@ -0,0 +1,45 @@
1
+ // Types for shared/repeats.mjs, which is plain ESM so that main (Node) and the
2
+ // renderer (Vite) can share one copy of the scheduling rule.
3
+
4
+ // The SCHEDULE, apart from the rule that carries it: the composer and the reply
5
+ // box hand this around before any rule exists.
6
+ export interface RepeatShape {
7
+ every: 'day' | 'weekday' | 'week';
8
+ on?: number; // 0..6 when every is 'week'
9
+ at: string; // 24h local, "09:00"
10
+ }
11
+
12
+ export interface RepeatRule extends RepeatShape {
13
+ id: string;
14
+ title: string;
15
+ body?: string;
16
+ priority?: number;
17
+ createdAt: number;
18
+ updatedAt?: number;
19
+ endedAt?: number;
20
+ served: string;
21
+ misses: number;
22
+ alerted: number;
23
+ lastOccurrence?: string;
24
+ }
25
+
26
+ export const CLEAN_LABEL: string;
27
+ export const REPEAT_LABEL_PREFIX: string;
28
+ export function repeatLabel(ruleId: string): string;
29
+ export function ruleIdOf(item: { labels?: string[] } | null | undefined): string | null;
30
+ export function isCleanRun(item: { labels?: string[] } | null | undefined): boolean;
31
+ export const DAY_NAMES: string[];
32
+ export function isRepeatRule(rule: unknown): boolean;
33
+ export function ruleLabel(rule: unknown): string;
34
+ export function clockLabel(at: string): string;
35
+ export function dateKey(now?: number): string;
36
+ export function periodKey(rule: Partial<RepeatRule>, now?: number): string | null;
37
+ export function nextRunAt(rule: Partial<RepeatRule>, now?: number): number | null;
38
+ export function sameRule(a: unknown, b: unknown): boolean;
39
+ export function servedOf(rule: Partial<RepeatRule>): string;
40
+ export function isOwed(rule: Partial<RepeatRule>, now?: number): boolean;
41
+ export function occurrenceId(ruleId: string, key: string): string;
42
+ export function foldRepeats(
43
+ lines: Array<{ id: string; ts: number; patch: Record<string, unknown> } | null>,
44
+ now?: number,
45
+ ): Map<string, RepeatRule>;
@@ -0,0 +1,193 @@
1
+ // Repeating tasks: the pure rules, shared by the main process and the renderer
2
+ // the way shared/rank.mjs is, so the tick and the list cannot come to disagree
3
+ // about when something is due.
4
+ //
5
+ // A repeating task is a RULE, not a work item. The spec
6
+ // (docs/superpowers/specs/2026-08-12-repeating-tasks-design.md) carries the six
7
+ // reasons; the short one is that a work-item ledger is read from a bounded
8
+ // trailing window and a template has to outlive that.
9
+
10
+ // A period is a local CALENDAR date, never `now - 86_400_000`, which is wrong by
11
+ // an hour twice a year. One date is one period, which is what makes DST, travel
12
+ // and clock correction boring here instead of a source of double runs.
13
+ const KEY_RE = /^\d{4}-\d{2}-\d{2}$/;
14
+
15
+ export const CLEAN_LABEL = 'clean';
16
+ export const REPEAT_LABEL_PREFIX = 'repeat:';
17
+
18
+ export function repeatLabel(ruleId) {
19
+ return `${REPEAT_LABEL_PREFIX}${ruleId}`;
20
+ }
21
+
22
+ export function ruleIdOf(item) {
23
+ const found = (item?.labels ?? []).find((l) => typeof l === 'string' && l.startsWith(REPEAT_LABEL_PREFIX));
24
+ return found ? found.slice(REPEAT_LABEL_PREFIX.length) : null;
25
+ }
26
+
27
+ // A clean run is one the worker EXPLICITLY marked clean. Everything else is news
28
+ // and reaches her, including a run that died without saying anything: this
29
+ // predicate exists to hide, never to show, so it fails open by construction.
30
+ export function isCleanRun(item) {
31
+ return !!ruleIdOf(item) && (item?.labels ?? []).includes(CLEAN_LABEL);
32
+ }
33
+
34
+ export const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
35
+
36
+ // Three shapes, because they are the three a founder actually asks for:
37
+ // { every: 'day', at } every morning
38
+ // { every: 'weekday', at } Monday to Friday, which is most work
39
+ // { every: 'week', on: 0..6, at } Thursdays
40
+ // The `every` field was an object from the start so these would be additions
41
+ // rather than a migration, and this is that.
42
+ export function isRepeatRule(rule) {
43
+ if (!rule) return false;
44
+ if (typeof rule.at !== 'string' || !/^([01]\d|2[0-3]):[0-5]\d$/.test(rule.at)) return false;
45
+ if (rule.every === 'day' || rule.every === 'weekday') return true;
46
+ if (rule.every === 'week') return Number.isInteger(rule.on) && rule.on >= 0 && rule.on <= 6;
47
+ return false;
48
+ }
49
+
50
+ // the tag, the row and the picker's preview all print this, so they cannot
51
+ // drift.
52
+ export function ruleLabel(rule) {
53
+ if (!isRepeatRule(rule)) return 'Once';
54
+ const time = clockLabel(rule.at);
55
+ if (rule.every === 'day') return `daily at ${time}`;
56
+ if (rule.every === 'weekday') return `weekdays at ${time}`;
57
+ return `${DAY_NAMES[rule.on]}s at ${time}`;
58
+ }
59
+
60
+ export function clockLabel(at) {
61
+ const [h, m] = at.split(':').map(Number);
62
+ const d = new Date();
63
+ d.setHours(h, m, 0, 0);
64
+ return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
65
+ .toLowerCase().replace(/\s/g, '');
66
+ }
67
+
68
+ // Does this calendar date belong to the rule at all? A weekday rule skips the
69
+ // weekend and a weekly rule wants one day, which is the only thing that changes
70
+ // about the period: it is still a date, still one per rule, still never
71
+ // arithmetic.
72
+ function servesDay(rule, d) {
73
+ if (rule.every === 'day') return true;
74
+ if (rule.every === 'weekday') return d.getDay() >= 1 && d.getDay() <= 5;
75
+ return d.getDay() === rule.on;
76
+ }
77
+
78
+ export function dateKey(now = Date.now()) {
79
+ const d = new Date(now);
80
+ const m = String(d.getMonth() + 1).padStart(2, '0');
81
+ const day = String(d.getDate()).padStart(2, '0');
82
+ return `${d.getFullYear()}-${m}-${day}`;
83
+ }
84
+
85
+ // The period `now` falls in: today's date once the hour has passed, yesterday's
86
+ // before it. Built with calendar operations so a clock change cannot shift it. A
87
+ // time that does not exist on a spring-forward date (02:30) normalizes to the
88
+ // first minute that does, which is exactly when that period should be served.
89
+ export function periodKey(rule, now = Date.now()) {
90
+ if (!isRepeatRule(rule)) return null;
91
+ const [h, m] = rule.at.split(':').map(Number);
92
+ const d = new Date(now);
93
+ const boundary = new Date(d.getFullYear(), d.getMonth(), d.getDate(), h, m, 0, 0);
94
+ if (boundary.getTime() > now) boundary.setDate(boundary.getDate() - 1);
95
+ // Walk back to the most recent day this rule actually serves. A week is the
96
+ // furthest that can ever be, so the loop is bounded and a rule whose day has
97
+ // not come round yet correctly reports the LAST one it served.
98
+ for (let back = 0; back < 8; back++) {
99
+ if (servesDay(rule, boundary)) return dateKey(boundary.getTime());
100
+ boundary.setDate(boundary.getDate() - 1);
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // The moment this rule next runs, for the Scheduled row.
106
+ export function nextRunAt(rule, now = Date.now()) {
107
+ if (!isRepeatRule(rule) || rule.endedAt) return null;
108
+ const [h, m] = rule.at.split(':').map(Number);
109
+ const d = new Date(now);
110
+ const next = new Date(d.getFullYear(), d.getMonth(), d.getDate(), h, m, 0, 0);
111
+ if (next.getTime() <= now) next.setDate(next.getDate() + 1);
112
+ for (let ahead = 0; ahead < 8; ahead++) {
113
+ if (servesDay(rule, next)) return next.getTime();
114
+ next.setDate(next.getDate() + 1);
115
+ }
116
+ return null;
117
+ }
118
+
119
+ // Two rules, same schedule? Used where typing drives the control: adopting a
120
+ // rule the words already say would otherwise fight her own pick every keystroke.
121
+ export function sameRule(a, b) {
122
+ if (!a || !b) return a === b;
123
+ return a.every === b.every && a.at === b.at && (a.on ?? null) === (b.on ?? null);
124
+ }
125
+
126
+ export function servedOf(rule) {
127
+ return KEY_RE.test(rule?.served ?? '') ? rule.served : '';
128
+ }
129
+
130
+ export function isOwed(rule, now = Date.now()) {
131
+ if (rule?.endedAt) return false;
132
+ const key = periodKey(rule, now);
133
+ return !!key && key > servedOf(rule);
134
+ }
135
+
136
+ // Deterministic, so two writers racing describe the SAME item rather than two
137
+ // rows nothing can merge. Create-if-absent under the lock is what makes serving
138
+ // idempotent; this is what makes that check possible at all.
139
+ export function occurrenceId(ruleId, key) {
140
+ return `${ruleId}-${key.replace(/-/g, '')}`;
141
+ }
142
+
143
+ // `engine` and `model` are the LAST two and they are the reason this list is
144
+ // worth reading twice: a field written by `setRule` and missing from here is
145
+ // folded away, so it reads correctly once and is gone at the next app start.
146
+ // That is exactly how `on` disappeared off every weekly rule.
147
+ const FIELDS = [
148
+ 'title', 'body', 'priority', 'every', 'at', 'on', 'engine', 'model',
149
+ 'createdAt', 'endedAt', 'served', 'misses', 'alerted', 'lastOccurrence',
150
+ ];
151
+
152
+ // Newest-wins per field, EXCEPT served, which folds by maximum period and
153
+ // refuses a date later than today. Same append-only shape as dashboard.jsonl,
154
+ // and the same reason: several writers, no read-modify-write, nothing lost.
155
+ //
156
+ // The two exceptions are one guard each against a watermark that lies. A
157
+ // delayed or clock-skewed writer landing an older date last would re-run a
158
+ // period already served, so the maximum wins rather than the latest line. And a
159
+ // date in the FUTURE cannot be legitimate (a period key is never later than
160
+ // today), so it is rejected outright: accepted, it would silence the task until
161
+ // the real calendar caught up, which could be years, and no later correct write
162
+ // could undo it because the maximum would keep it forever.
163
+ export function foldRepeats(lines, now = Date.now()) {
164
+ const today = dateKey(now);
165
+ const rules = new Map();
166
+ for (const line of lines) {
167
+ if (!line || typeof line !== 'object') continue;
168
+ const { id, ts, patch } = line;
169
+ if (typeof id !== 'string' || !id || !patch || typeof patch !== 'object') continue;
170
+ if (!Number.isFinite(ts)) continue;
171
+ let rule = rules.get(id);
172
+ if (!rule) {
173
+ rule = { id, createdAt: ts, updatedAt: ts, served: '', misses: 0, alerted: 0, _ts: {} };
174
+ rules.set(id, rule);
175
+ }
176
+ for (const field of FIELDS) {
177
+ if (!(field in patch)) continue;
178
+ const value = patch[field];
179
+ if (field === 'served') {
180
+ if (KEY_RE.test(value ?? '') && value <= today && value > rule.served) rule.served = value;
181
+ continue;
182
+ }
183
+ const held = rule._ts[field];
184
+ if (held !== undefined && ts < held) continue;
185
+ rule[field] = value;
186
+ rule._ts[field] = ts;
187
+ }
188
+ rule.createdAt = Math.min(rule.createdAt, ts);
189
+ rule.updatedAt = Math.max(rule.updatedAt, ts);
190
+ }
191
+ for (const rule of rules.values()) delete rule._ts;
192
+ return rules;
193
+ }
@@ -0,0 +1,33 @@
1
+ // THE APP'S TWO PRIVATE URL SCHEMES, NAMED FROM THE ONE PLACE THE APP IS NAMED.
2
+ //
3
+ // These were typed out as `astral-doc` and `astral-img`, two renames after the
4
+ // app stopped being called that, in main, in the renderer and in the tests. An
5
+ // agent reading any of them learns the wrong name for the app it is working
6
+ // inside, which is the fault the founder opened the row about, and the next
7
+ // rename would have had to find them all by hand.
8
+ //
9
+ // EVERY OLD SPELLING IS STILL SERVED, AND THAT IS NOT POLITENESS. A url on one
10
+ // of these schemes is STORED: a product's logo is kept in the store as
11
+ // `<scheme>://file/…` and a message she has already been sent holds picture
12
+ // urls in its body. Dropping the old scheme would not degrade those, it would
13
+ // break them, and the symptom is a broken image in a row she wrote months ago.
14
+ // So the current name is what new urls are built with, and every name this app
15
+ // has had is still registered and still answered.
16
+
17
+ import { WAS, nameSlug } from './product-name.mjs';
18
+
19
+ const under = (was) => was.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
20
+
21
+ /** What a url built today is on. */
22
+ export const DOC_SCHEME = `${nameSlug}-doc`;
23
+ export const IMG_SCHEME = `${nameSlug}-img`;
24
+
25
+ /** Every spelling that must still resolve, current name first. */
26
+ export const DOC_SCHEMES = Object.freeze([DOC_SCHEME, ...WAS.map((was) => `${under(was)}-doc`)]);
27
+ export const IMG_SCHEMES = Object.freeze([IMG_SCHEME, ...WAS.map((was) => `${under(was)}-img`)]);
28
+
29
+ /** Both, for the one place that registers all of them at once. */
30
+ export const ALL_SCHEMES = Object.freeze([...DOC_SCHEMES, ...IMG_SCHEMES]);
31
+
32
+ /** True when `protocol` (as `URL` gives it, with its colon) is one of them. */
33
+ export const isScheme = (schemes, protocol) => schemes.some((s) => `${s}:` === protocol);
@@ -0,0 +1 @@
1
+ export declare const SETTINGS_TERMINAL: { product: string; id: string };
@@ -0,0 +1,14 @@
1
+ // THE ONE TERMINAL THAT BELONGS TO NO TASK.
2
+ //
3
+ // Every other shell in this app is opened against a work item, and
4
+ // `terminalFolder` refuses a key whose item does not exist. Signing a second
5
+ // Codex account in needs a shell and has no item, so it gets this one reserved
6
+ // key, and main resolves it to the folder main itself just made rather than to
7
+ // anything the window asked for.
8
+ //
9
+ // THE PRODUCT WORD CARRIES A COLON ON PURPOSE. A key is `{product, id}`, and a
10
+ // product here is a slug taken from a folder name; a colon never survives that,
11
+ // so no project anybody creates can collide with this and be handed a shell
12
+ // pointed somewhere else. Both sides import this rather than spelling it,
13
+ // because the whole value of a reserved key is that there is one of it.
14
+ export const SETTINGS_TERMINAL = { product: 'zero:settings', id: 'codex-login' };
@@ -0,0 +1,57 @@
1
+ // A SIDE BUILD IS A SECOND BUNDLE AND IT IS NOT A SECOND AGENTBOX.
2
+ //
3
+ // 2026-08-27.
4
+ //
5
+ // The bundle is separate. The DATA FOLDER MUST NOT BE, and this file is the one
6
+ // place that says so, because two files saying it is how one of them goes
7
+ // quietly wrong.
8
+ //
9
+ // Why it matters, in main.mjs's own words at the single-instance lock: "Two
10
+ // instances mean two supervisors, and two supervisors spawn twin workers for
11
+ // every answered item (observed 2026-08-04: nine items double-spawned one
12
+ // second apart, four agents told the founder the same thing)." That lock lives
13
+ // INSIDE the data folder — measured on this Mac 2026-08-27, the running Agentbox
14
+ // holds SingletonLock in ~/Library/Application Support/Astral — so two folders
15
+ // would be two locks and the protection would simply not be there.
16
+ //
17
+ // Sharing the folder also means the side build is not a stranger's copy: her
18
+ // store, her settings, her theme, her place in everything, plus the branch.
19
+
20
+ import { NAME } from '../shared/product-name.mjs';
21
+
22
+ /**
23
+ * THE BUNDLE NAME, not a sentence, so it is the plain spelling however the
24
+ * name is capitalised. Everything here compares against what macOS calls the
25
+ * bundle, which is `productName` in package.json, which is this. */
26
+ export const REAL_NAME = NAME;
27
+
28
+ /**
29
+ * The app's own name and a word after it, yes. The bare name itself, no, that
30
+ * is the real one. A bare word, or the name run into a word with no space, no:
31
+ * a name that does not read as the app plus a word is a name
32
+ * nobody will recognise in the Dock, and pack-side refuses it rather than
33
+ * quietly building a second supervisor. */
34
+ export function isSideBuild(name) {
35
+ const word = REAL_NAME.replace(/[.*+?^${}()|[\]\\]/g, (c) => `\\${c}`);
36
+ return new RegExp(`^${word} .+`).test(name ?? '');
37
+ }
38
+
39
+ // THE ONE SIDE BUILD THAT MUST NOT SHARE HER FOLDER.
40
+ //
41
+ // That folder has to be its OWN, or the hand-over would take the real
42
+ // Agentbox's single-instance lock and bring her inbox to the front instead of
43
+ // opening a stranger's.
44
+ export const NEW_USER_NAME = `${REAL_NAME} new user`;
45
+
46
+ export function isNewUserBuild(name) {
47
+ return name === NEW_USER_NAME;
48
+ }
49
+
50
+ /**
51
+ * The folder Chromium should name after this app, whatever the bundle is
52
+ * called. Everything downstream keys off app.getName, so a side build sets
53
+ * its name back to this before anything reads a path. */
54
+ export function dataFolderName(name) {
55
+ if (isNewUserBuild(name)) return name;
56
+ return isSideBuild(name) ? REAL_NAME : name;
57
+ }
@@ -0,0 +1,8 @@
1
+ // Types for shared/spawn-trouble.mjs, which is plain ESM so that the
2
+ // supervisor (Node) and the renderer (Vite) share one copy of every sentence
3
+ // this app is allowed to say about a task that could not run.
4
+ //
5
+ // Only what the renderer imports is declared. The rest of the module is read by
6
+ // main and by the tests, which are both plain JS and need nothing here.
7
+
8
+ export function strandedTitle(count: number): string;