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,261 @@
1
+ // The rule store: the only writer of <product>/repeats.jsonl.
2
+ //
3
+ // A repeating task is a RULE, never a work item, so it never runs, is never
4
+ // claimed, and cannot be finished by a worker. Its runs are ordinary work items
5
+ // and go through the store's modules like everything else.
6
+ //
7
+ // The file is append-only and folded newest-wins, the same shape as
8
+ // dashboard.jsonl, and it is read WHOLE. That is the point rather than a
9
+ // detail: the work-item ledger is read from a trailing 8 MiB window, and the
10
+ // fold applies only the fields present in surviving lines, so a long-lived
11
+ // template kept there would eventually lose the rule itself and stop repeating
12
+ // with no symptom at all.
13
+
14
+ import crypto from 'node:crypto';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import {
18
+ DAY_NAMES, foldRepeats, isOwed, isRepeatRule, occurrenceId, periodKey, repeatLabel,
19
+ } from '../shared/repeats.mjs';
20
+ import { machineryPath } from './store/home.mjs';
21
+ import { NAME } from '../shared/product-name.mjs';
22
+
23
+ const FILE = 'repeats.jsonl';
24
+
25
+ // What a refused rule is called when we say we cannot keep it. A weekly rule
26
+ // whose day is missing used to print "every week at 08:00", which names the one
27
+ // part that was fine and hides the part that was not.
28
+ function describeRule({ every, at, on }) {
29
+ if (every === 'week') {
30
+ const day = DAY_NAMES[on] ?? 'no day';
31
+ return `every week on ${day} at ${at}`;
32
+ }
33
+ return `every ${every} at ${at}`;
34
+ }
35
+
36
+ // How many periods in a row may be superseded before the founder is told.
37
+ const MISSES_BEFORE_ALERT = 3;
38
+
39
+ // The protocol goes FIRST in an occurrence's body, ahead of her words. An
40
+ // over-long body keeps its prefix and cuts its suffix, so a protocol written
41
+ // last can be truncated away entirely, and a worker that never saw it cannot
42
+ // know that a quiet finish has to be marked.
43
+ export const OCCURRENCE_PROTOCOL = [
44
+ 'THIS IS ONE RUN OF A REPEATING TASK. It runs again tomorrow whatever happens',
45
+ 'here, so this run does not have to carry the future.',
46
+ '',
47
+ 'FINISH ONE OF TWO WAYS.',
48
+ '',
49
+ 'Nothing she needs to know: update this item with status done AND labels',
50
+ '["founder", "<REPEAT_LABEL>", "clean"] IN THE SAME CALL. Labels REPLACE the',
51
+ 'array rather than merging, so send all three. The clean label is what keeps a',
52
+ 'quiet run out of her inbox, and no result is needed: that is what clean means.',
53
+ '',
54
+ 'Anything she needs to know: finish normally, with a result saying what you',
55
+ 'found, and do NOT set the clean label. It reaches her inbox, which is the',
56
+ 'point of running this every day. Anything that deserves its own life gets',
57
+ 'filed as its own item.',
58
+ '',
59
+ 'If you are unsure which of the two this is, it is the second one. This is the',
60
+ `only place in ${NAME} where finishing quietly hides something, so the marker is`,
61
+ 'required to hide, never to show.',
62
+ '',
63
+ 'HER INSTRUCTION FOLLOWS.',
64
+ ].join('\n');
65
+
66
+ export class Repeats {
67
+ constructor(storeModules) {
68
+ this.modules = storeModules;
69
+ }
70
+
71
+ // In the app's home, not the product folder: a repeat rule is our record of
72
+ // what to run, not one of her documents (main/store/home.mjs).
73
+ filePath(productDir) {
74
+ return machineryPath(productDir, FILE);
75
+ }
76
+
77
+ _read(productDir) {
78
+ let raw = '';
79
+ try {
80
+ raw = fs.readFileSync(this.filePath(productDir), 'utf8');
81
+ } catch (err) {
82
+ if (err.code === 'ENOENT') return [];
83
+ throw err;
84
+ }
85
+ return raw.split('\n').filter(Boolean).map((line) => {
86
+ // A torn line is skipped rather than thrown on, the way every other fold
87
+ // in this store treats one.
88
+ try { return JSON.parse(line); } catch { return null; }
89
+ });
90
+ }
91
+
92
+ _append(productDir, id, patch, now = Date.now()) {
93
+ fs.appendFileSync(this.filePath(productDir), `${JSON.stringify({ id, ts: now, patch })}\n`);
94
+ }
95
+
96
+ list(productDir, now = Date.now()) {
97
+ return [...foldRepeats(this._read(productDir), now).values()];
98
+ }
99
+
100
+ get(productDir, id, now = Date.now()) {
101
+ return foldRepeats(this._read(productDir), now).get(id) ?? null;
102
+ }
103
+
104
+ // `engine` AND `model` ARE HER PICK ON THE RULE, and the runs carry them
105
+ // (`serve` below). They are optional and absent by default, which is every
106
+ // rule on her disk: a rule that names neither behaves exactly as it did, and
107
+ // its runs go to the workspace default like any unmarked row. Whether either
108
+ // word may be written at all is decided at the door, by
109
+ // `Supervisor#engineOffered` and `#modelOffered`, so a rule cannot be marked
110
+ // for an engine this Mac never offered her.
111
+ setRule(productDir, {
112
+ title, body, priority = 5, every = 'day', at, on, engine, model,
113
+ }, now = Date.now()) {
114
+ // Refused here rather than stored and ignored later: a rule that cannot be
115
+ // kept has to fail while she is still looking at what she typed.
116
+ //
117
+ // `on` is validated AND stored. It was in neither, so a weekly rule was
118
+ // checked without the day that makes it weekly, and no weekly rule ever
119
+ // survived this line (hers). The refusal says which day is missing now,
120
+ // because "every week at 08:00" reads like a bug in the clock.
121
+ if (!title || !String(title).trim()) throw new Error('a repeating task needs a title');
122
+ if (!isRepeatRule({ every, at, on })) throw new Error(`not a schedule ${NAME} can keep: ${describeRule({ every, at, on })}`);
123
+ const id = `r-${crypto.randomBytes(5).toString('hex')}`;
124
+ this._append(productDir, id, {
125
+ title: String(title).trim(), body, priority, every, at,
126
+ ...(every === 'week' ? { on } : {}),
127
+ ...(engine ? { engine: String(engine) } : {}),
128
+ ...(model ? { model: String(model) } : {}),
129
+ createdAt: now, served: '', misses: 0, alerted: 0,
130
+ }, now);
131
+ return this.get(productDir, id, now);
132
+ }
133
+
134
+ patchRule(productDir, id, patch, now = Date.now()) {
135
+ // `on` joins the other two: changing a daily rule to a Thursday one sends
136
+ // every, at AND on, and checking the first two alone refused the change for
137
+ // the same reason a new weekly rule was refused (hers).
138
+ if (('at' in patch || 'every' in patch || 'on' in patch)) {
139
+ const current = this.get(productDir, id, now);
140
+ if (!current) throw new Error(`no such repeating task: ${id}`);
141
+ const next = {
142
+ every: patch.every ?? current.every,
143
+ at: patch.at ?? current.at,
144
+ on: patch.on ?? current.on,
145
+ };
146
+ if (!isRepeatRule(next)) throw new Error(`not a schedule ${NAME} can keep: ${describeRule(next)}`);
147
+ }
148
+ this._append(productDir, id, patch, now);
149
+ return this.get(productDir, id, now);
150
+ }
151
+
152
+ // Ending one is a small durable fact rather than an archive, so it does not
153
+ // collide with what `done` means on a work item.
154
+ endRule(productDir, id, now = Date.now()) {
155
+ return this.patchRule(productDir, id, { endedAt: now }, now);
156
+ }
157
+
158
+ // The rules whose current period has not been served. Never more than one
159
+ // period per rule: there is no queue of missed firings, only a condition that
160
+ // is currently false, which is why three days away costs one run.
161
+ due(productDir, now = Date.now()) {
162
+ return this.list(productDir, now)
163
+ .filter((rule) => isOwed(rule, now))
164
+ .map((rule) => ({ rule, key: periodKey(rule, now) }));
165
+ }
166
+
167
+ /**
168
+ * Serve one period. ONE short locked step of local reads and appends: create
169
+ * the occurrence if its id is absent, supersede an unfinished predecessor,
170
+ * then move the watermark.
171
+ *
172
+ * Nothing slow happens inside the lock. The session is spawned by the caller
173
+ * afterwards, and claimWorkItem is never called here because it takes the
174
+ * same lock and this one is not reentrant.
175
+ *
176
+ * The order is create-then-mark, so a crash between them retries into a
177
+ * create that finds the id present and does nothing, rather than running the
178
+ * work twice.
179
+ */
180
+ async serve(productDir, ruleId, key, now = Date.now()) {
181
+ const { workItemsDisk, lock } = this.modules;
182
+ return lock.withProjectLock(productDir, async () => {
183
+ const rule = this.get(productDir, ruleId, now);
184
+ const nothing = { occurrenceId: null, created: false, superseded: null, misses: rule?.misses ?? 0, alert: null };
185
+ if (!rule || rule.endedAt || !isRepeatRule(rule)) return nothing;
186
+
187
+ const id = occurrenceId(ruleId, key);
188
+ let superseded = null;
189
+ let misses = rule.misses ?? 0;
190
+
191
+ // The predecessor's state is re-read HERE, inside the lock. A worker
192
+ // finishing between the tick's read and this write is the ordinary case,
193
+ // and superseding a run that has just succeeded reports a failure that
194
+ // never happened.
195
+ //
196
+ // Never the item we are about to create: a rule whose watermark was
197
+ // refused could otherwise be asked to serve the same period twice and
198
+ // supersede its own live run.
199
+ if (rule.lastOccurrence && rule.lastOccurrence !== id) {
200
+ const prev = workItemsDisk.readWorkItem(productDir, rule.lastOccurrence, now);
201
+ if (prev && (prev.status === 'open' || prev.status === 'claimed')) {
202
+ workItemsDisk.updateWorkItem(productDir, prev.id, {
203
+ status: 'done',
204
+ result: 'This run did not finish before the next one was due, so it was superseded.',
205
+ }, { source: 'system', now });
206
+ superseded = prev.id;
207
+ misses += 1;
208
+ } else if (prev && prev.status === 'done') {
209
+ misses = 0;
210
+ }
211
+ }
212
+
213
+ const { created } = workItemsDisk.createWorkItemIfAbsent(productDir, id, {
214
+ title: `${rule.title} · ${dayStamp(key)}`,
215
+ body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${rule.body ?? rule.title}`,
216
+ kind: 'directive',
217
+ priority: rule.priority ?? 5,
218
+ labels: ['founder', repeatLabel(ruleId)],
219
+ // WHICH CODING AGENT AND WHICH MODEL, off the rule, at the moment the
220
+ // run is made. A run is an ordinary work item, so this is the same pair
221
+ // `composeItem` writes for a task she types by hand and it is read by
222
+ // the same `engineFor` -- the occurrence needs no rule-shaped special
223
+ // case anywhere downstream. Absent on a rule that names neither, which
224
+ // is every rule on her disk today.
225
+ ...(rule.engine ? { engine: rule.engine } : {}),
226
+ ...(rule.model ? { model: rule.model } : {}),
227
+ }, { source: 'system', now });
228
+
229
+ // Told once per streak, and that is a durable fact rather than an
230
+ // intention: a crash after filing cannot file it twice, and one before it
231
+ // cannot lose it.
232
+ const alertNeeded = misses >= MISSES_BEFORE_ALERT && misses > (rule.alerted ?? 0);
233
+ this._append(productDir, ruleId, {
234
+ served: key,
235
+ lastOccurrence: id,
236
+ misses,
237
+ ...(alertNeeded ? { alerted: misses } : {}),
238
+ }, now);
239
+
240
+ return { occurrenceId: id, created, superseded, misses, alert: alertNeeded ? missedKeys(key, misses) : null };
241
+ }, { label: `repeat ${ruleId}` });
242
+ }
243
+ }
244
+
245
+ function dayStamp(key) {
246
+ const [y, m, d] = key.split('-').map(Number);
247
+ return new Date(y, m - 1, d).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
248
+ }
249
+
250
+ // The period keys the misses covered, oldest first, for the alert's wording.
251
+ function missedKeys(currentKey, misses) {
252
+ const [y, m, d] = currentKey.split('-').map(Number);
253
+ const keys = [];
254
+ for (let back = misses; back >= 1; back--) {
255
+ const day = new Date(y, m - 1, d - back);
256
+ const mm = String(day.getMonth() + 1).padStart(2, '0');
257
+ const dd = String(day.getDate()).padStart(2, '0');
258
+ keys.push(`${day.getFullYear()}-${mm}-${dd}`);
259
+ }
260
+ return keys;
261
+ }
@@ -0,0 +1,185 @@
1
+ // THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM HER FIRST SENTENCE.
2
+ //
3
+ // A message she dictates keeps its first line as its title, verbatim and
4
+ // forever (renderer/src/message-split.ts). Measured on her Agentbox store the day
5
+ // she asked: 214 of the 330 items are hers, median title 84 characters, 145 over
6
+ // 60, 37 cut off mid sentence with an ellipsis, and 76 opening with "I", "I've
7
+ // been", "Can you" or "We". Eleven of those down a column is eleven rows that
8
+ // look like one row.
9
+ //
10
+ // THERE IS NOTHING TO EXTRACT FROM CLAUDE CODE, which is the first thing this
11
+ // had to find out. Claude Code does generate a short name for a session and it
12
+ // writes it to the terminal tab; it never puts it on disk. Measured 2026-09-03:
13
+ // ZERO of her 3,126 transcript files written since 20 August carry a `summary`
14
+ // row, and main/agent-sessions.mjs found the same over 495 files on 08-29. So
15
+ // the name is not read from anywhere. It is written, here, by the same kind of
16
+ // call Claude Code makes: one small fast model, one message, one line back.
17
+ //
18
+ // AND IT IS WRITTEN FROM WHERE THE THREAD STANDS, NOT FROM ITS FIRST MESSAGE.
19
+ // This is the difference she was shown and the one she picked. Titled from its
20
+ // opening line alone came back "Fix onboarding and theme visibility", which was
21
+ // true the day she wrote it and by then the row was about landing five
22
+ // branches; given the last thing written on the row as well, the same call said
23
+ // "Landing branches for app fixes". Her threads run for days, so a name fixed
24
+ // at the first message goes stale the way rule 8 of her instructions is about,
25
+ // and this is the same failure arriving from the other side.
26
+ //
27
+ // WHY THE APP DOES THIS AND NOT THE WORKER. It was written into briefs/worker.md
28
+ // first and taken straight back out: the brief is pasted into every session and
29
+ // tests/the-brief-only-says-what-applies.mjs holds it under 6,000 characters,
30
+ // which it was 33 characters short of. A rule every worker must remember is also
31
+ // a rule most of them will not, and the row she is looking at right now may have
32
+ // no worker on it at all. Doing it here names every row on every product with
33
+ // nothing for anyone to remember.
34
+ //
35
+ // The write goes to `label`, never to `title`. The title is hers, the fold makes
36
+ // her outrank us on it, and she still reads her own words the moment she opens
37
+ // the row (shared/work-items.mjs).
38
+
39
+ import { spawn } from 'node:child_process';
40
+
41
+ /**
42
+ * The model this runs on. Small and fast on purpose: this is a naming call, it
43
+ * happens on a timer, and it must never be worth thinking about. Measured
44
+ * through the CLI on 2026-09-03 over ten of her real rows: 14.1s median a call,
45
+ * nearly all of it the CLI starting a session rather than the model working. */
46
+ export const NAME_MODEL = 'claude-haiku-4-5-20251001';
47
+
48
+ /**
49
+ * How long one naming call may take before it is abandoned. Generous against
50
+ * the 14.1s median because a slow one costs nothing: the row keeps the name it
51
+ * already had, or her title, and the next tick tries again. */
52
+ export const NAME_TIMEOUT_MS = 90_000;
53
+
54
+ /**
55
+ * What a name may be. Long enough for "Landing branches for app fixes", short
56
+ * enough that it can never be the thing that makes a row clip: the row draws
57
+ * one line and the summary budget beside it is 112. */
58
+ export const NAME_MAX = 64;
59
+
60
+ /**
61
+ * A row nobody is going to look at again does not get named. `done` is the
62
+ * archive; everything else is a row that can still appear in a list. */
63
+ const LIVE = new Set(['open', 'claimed', 'blocked']);
64
+
65
+ /**
66
+ * The newest moment anything about this row's CONTENT changed. Deliberately not
67
+ * `updatedAt`: a heartbeat, a claim and a release all move that, and renaming a
68
+ * row every time a worker touches it would spend a call a minute on a busy row
69
+ * and change the words under her while she reads them. */
70
+ export function contentTouchedAt(item) {
71
+ const w = item?.wrote ?? {};
72
+ let last = 0;
73
+ for (const field of ['title', 'body', 'result', 'answer']) {
74
+ const ts = w[field]?.ts ?? 0;
75
+ if (ts > last) last = ts;
76
+ }
77
+ return last;
78
+ }
79
+
80
+ /**
81
+ * PURE. Does this row want naming?
82
+ *
83
+ * Three ways to say no, and the third is the one that keeps this cheap:
84
+ * - it is done, so nobody is scanning for it,
85
+ * - it has nothing to name from, which is a row with no words on it yet,
86
+ * - its label is already at least as new as the newest thing said on the row.
87
+ *
88
+ * A row whose label we wrote and whose content has not moved since is finished
89
+ * business forever, so a store that has settled costs nothing per tick.
90
+ */
91
+ export function wantsName(item) {
92
+ if (!item || !LIVE.has(item.status)) return false;
93
+ if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
94
+ const wroteAt = item.wrote?.label?.ts ?? 0;
95
+ if (!String(item.label ?? '').trim()) return true;
96
+ return wroteAt < contentTouchedAt(item);
97
+ }
98
+
99
+ /**
100
+ * PURE. The one message the naming call gets. Her words first, then the last
101
+ * thing written on the row, which is what makes the name describe the thread
102
+ * rather than its opening line. Truncated hard: a name does not get better for
103
+ * having read six thousand words, and this is spawned on a timer. */
104
+ export function namePrompt(item) {
105
+ const cut = (s, n) => String(s ?? '').replace(/\r/g, '').trim().slice(0, n);
106
+ const latest = cut(item?.result || item?.note, 700);
107
+ return [
108
+ 'Here is a work thread: the message that opened it, and the latest thing',
109
+ 'written on it. Write a short name for the thread AS IT STANDS NOW: 3 to 7',
110
+ 'words, sentence case, no quotes, no trailing punctuation, naming the thing',
111
+ 'being worked on rather than restating the sentence. Reply with the name and',
112
+ 'nothing else.',
113
+ '',
114
+ '--- opened with ---',
115
+ cut(item?.title, 300),
116
+ cut(item?.body, 1200),
117
+ '--- latest ---',
118
+ latest || '(nothing written yet)',
119
+ '---',
120
+ ].join('\n');
121
+ }
122
+
123
+ /**
124
+ * PURE. What comes back from a model is not a name until this has had it.
125
+ *
126
+ * A one-line reply is the contract and most of the time it is what arrives. The
127
+ * rest of this is what happens when it does not: a preamble, a quoted name, a
128
+ * trailing full stop, or a paragraph explaining the choice. Anything that fails
129
+ * every rule returns '' and the row keeps the name it had, which is always safe
130
+ * because the fallback is her own title.
131
+ */
132
+ export function cleanName(text) {
133
+ const first = String(text ?? '')
134
+ .split('\n')
135
+ .map((l) => l.trim())
136
+ .filter(Boolean)[0] ?? '';
137
+ const bare = first
138
+ .replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, '')
139
+ .replace(/^(?:title|name)\s*[:\-–]\s*/i, '')
140
+ .replace(/[.。]+$/, '')
141
+ .trim();
142
+ if (!bare || bare.length > NAME_MAX) return '';
143
+ // A reply that runs on is a model that answered a different question, and a
144
+ // chatty one clears the character cap easily. The prompt asks for three to
145
+ // seven words, so eight is one word of slack and anything past it is not the
146
+ // thing that was asked for.
147
+ if (bare.split(/\s+/).length > 8) return '';
148
+ return bare;
149
+ }
150
+
151
+ /**
152
+ * Name one row. Resolves to a clean name, or to '' for every failure there is:
153
+ * the binary missing, a non-zero exit, a timeout, an empty reply, a reply that
154
+ * is not a name. Never throws and never rejects, because this is called from a
155
+ * timer whose only correct behaviour on failure is to leave the row alone.
156
+ */
157
+ export function nameRow(item, { claudeBin, model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS } = {}) {
158
+ return new Promise((resolve) => {
159
+ if (!claudeBin) return resolve('');
160
+ let child;
161
+ try {
162
+ child = spawn(claudeBin, ['-p', namePrompt(item), '--model', model], {
163
+ // Somewhere that is nobody's project. A naming call must not pick up a
164
+ // CLAUDE.md, a settings file or a hook from whatever folder it lands in.
165
+ cwd: '/tmp',
166
+ stdio: ['ignore', 'pipe', 'pipe'],
167
+ });
168
+ } catch {
169
+ return resolve('');
170
+ }
171
+ let out = '';
172
+ let done = false;
173
+ const finish = (value) => {
174
+ if (done) return;
175
+ done = true;
176
+ clearTimeout(timer);
177
+ try { child.kill('SIGKILL'); } catch { /* already gone */ }
178
+ resolve(value);
179
+ };
180
+ const timer = setTimeout(() => finish(''), timeoutMs);
181
+ child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(cleanName(out)); });
182
+ child.on('error', () => finish(''));
183
+ child.on('close', (code) => finish(code === 0 ? cleanName(out) : ''));
184
+ });
185
+ }
@@ -0,0 +1,44 @@
1
+ // WHICH SET OF PICTURES A SCREEN GETS., 2026-08-22.
2
+ //
3
+ // Kept out of main.mjs and free of Electron, like zoom-keys.mjs, so the rule can
4
+ // be tested against made-up displays instead of against whatever monitor the
5
+ // machine running the tests happens to have.
6
+ //
7
+ // WHY THE BUILT-IN PANEL AND NOT A WIDTH. Three numbers were on the table and
8
+ // two of them lie:
9
+ //
10
+ // - device pixels across the screen: her laptop reports 3420 and the 34" is
11
+ // 3440. A 20px gap is not a signal.
12
+ // - devicePixelRatio in the window: she runs the app at 120% zoom, so the
13
+ // laptop reads 2.4 and the 34" would read 1.2. Correct today and it flips
14
+ // the moment she presses Cmd+minus a few times, because Chromium folds page
15
+ // zoom into that number.
16
+ // - the display's own scaleFactor and `internal` flag, which the main process
17
+ // reads off the window server. Neither moves when the page zooms.
18
+ //
19
+ // So: the built-in panel of a Retina laptop is the small dense screen, and
20
+ // everything plugged into it is the big one. That is her sentence, not a
21
+ // threshold anybody has to defend later. Measured on her machine 2026-08-22
22
+ // through the running app: one display, `internal: true`, `scaleFactor: 2`,
23
+ // 1710x1107 points, window zoom 1.2.
24
+
25
+ /** @typedef {'soft' | 'sharp'} ScreenDetail */
26
+
27
+ /**
28
+ * Which picture set a display should paint.
29
+ *
30
+ * Anything unrecognisable answers 'sharp', because the sharp set is what every
31
+ * screen got before this existed and an unreadable display is not a reason to
32
+ * quietly downgrade one. `internal` is undefined on some platforms; that is a
33
+ * 'sharp' too, for the same reason.
34
+ *
35
+ * @param {{ internal?: boolean, scaleFactor?: number } | null | undefined} display
36
+ * @returns {ScreenDetail}
37
+ */
38
+ export function screenDetailFor(display) {
39
+ if (!display || typeof display !== 'object') return 'sharp';
40
+ if (display.internal !== true) return 'sharp';
41
+ const s = display.scaleFactor;
42
+ if (typeof s !== 'number' || !Number.isFinite(s)) return 'sharp';
43
+ return s >= 2 ? 'soft' : 'sharp';
44
+ }