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,377 @@
1
+ // Work items across every product: the unified queue.
2
+ //
3
+ // The substrate is deliberately thin. It knows how to hold an item, hand it to
4
+ // exactly one worker, and let go when that worker dies. It does NOT know what
5
+ // work is worth doing, what a milestone is, when to ask the founder, or whether
6
+ // a category has earned the right to act without asking. Every one of those is
7
+ // going to be rewritten several times as the surface above learns what it
8
+ // actually needs, and a substrate that had opinions about them would have to be
9
+ // rewritten alongside.
10
+ //
11
+ // The heartbeat lives here rather than in a tool, and that is a load-bearing
12
+ // choice. A heartbeat tool would depend on the model remembering to call it
13
+ // during work that is, by definition, absorbing its attention; it would forget,
14
+ // and a healthy worker would lose its claim mid-task, which is the worst version
15
+ // of this failure. Instead the SERVER PROCESS beats while it holds anything. The
16
+ // process lives exactly as long as the agent session that spawned it, so:
17
+ // session dies, child dies, beating stops, lease lapses, the item comes back.
18
+ // Liveness for free, and nothing to remember.
19
+
20
+ import path from 'node:path';
21
+ import { listProjects } from '../../main/store/project.mjs';
22
+ import {
23
+ readWorkItems, readWorkItem, createWorkItem, updateWorkItem,
24
+ claimWorkItem, heartbeatWorkItem, releaseWorkItem, LEASE_MS,
25
+ } from '../../main/store/work-items.mjs';
26
+ import { matchesFilter } from '../../shared/work-items.mjs';
27
+ import { unleaked } from '../../shared/agents.mjs';
28
+ import {
29
+ parkPendingWrite, readPendingWrites, collectPendingWrites,
30
+ } from '../../main/store/pending-writes.mjs';
31
+ import { resolveProduct } from './account.mjs';
32
+ import { nameSlug } from '../../shared/product-name.mjs';
33
+
34
+ // The log prefix, named from the one place the app is named. It was a literal
35
+ // for as long as this server lived in another product's repo.
36
+ const LOG = `[${nameSlug}-store]`;
37
+
38
+ // Beat at a third of the lease, so two consecutive misses (a busy event loop, a
39
+ // slow disk) still leave a full beat of margin before anything lapses.
40
+ const HEARTBEAT_MS = Math.floor(LEASE_MS / 3);
41
+
42
+ /** Every work item across every product, newest activity first. */
43
+ export function listWorkItems({ product = null, status = null, kind = null, labels = null, includeDone = false, now = Date.now() } = {}) {
44
+ const projects = product ? [resolveProduct(product)] : listProjects();
45
+ const out = [];
46
+
47
+ for (const p of projects) {
48
+ let items = [];
49
+ try { items = readWorkItems(p.dir, now); } catch { continue; } // a product with no ledger has no work
50
+ for (const item of items) {
51
+ if (!includeDone && item.status === 'done') continue;
52
+ if (status && item.status !== status) continue;
53
+ if (!matchesFilter(item, { kind, labels })) continue;
54
+ out.push({ ...item, product: p.id, productName: p.name });
55
+ }
56
+ }
57
+ return out.sort((a, b) => b.updatedAt - a.updatedAt);
58
+ }
59
+
60
+ /** Find which product owns an id, so callers do not have to track it. */
61
+ function locate(id, { product = null } = {}) {
62
+ if (product) {
63
+ const p = resolveProduct(product);
64
+ const item = readWorkItem(p.dir, id);
65
+ if (!item) throw new Error(`no work item ${id} in ${p.name}`);
66
+ return { project: p, item };
67
+ }
68
+ for (const p of listProjects()) {
69
+ let item = null;
70
+ try { item = readWorkItem(p.dir, id); } catch { continue; }
71
+ if (item) return { project: p, item };
72
+ }
73
+ throw new Error(`no work item ${id} in any product`);
74
+ }
75
+
76
+ // THE WORDS AN AGENT SENDS, WITH A LEAKED TOOL CALL CUT OFF THEM.
77
+ //
78
+ // `unleaked` holds what leaks and the row it cost. It is applied here, at the
79
+ // boundary where an agent's text arrives, and NOT in the store: the store also
80
+ // holds what she typed, and her words are hers whatever they contain. The pane
81
+ // cuts it again on the way out, because the rows that already carry it are
82
+ // still on her screen.
83
+ const WORDS = ['result', 'note', 'body', 'title', 'label'];
84
+
85
+ function saidPlainly(fields, source) {
86
+ if (source === 'founder' || !fields) return fields;
87
+ const out = { ...fields };
88
+ for (const f of WORDS) {
89
+ if (typeof out[f] === 'string') out[f] = unleaked(out[f]);
90
+ }
91
+ return out;
92
+ }
93
+
94
+ export function createItem(product, fields = {}, { source = 'agent' } = {}) {
95
+ const p = resolveProduct(product);
96
+ const item = createWorkItem(p.dir, saidPlainly(fields, source), { source });
97
+ return { ...item, product: p.id, productName: p.name };
98
+ }
99
+
100
+ export function updateItem(id, patch, { product = null, epoch = null, source = 'agent' } = {}) {
101
+ const { project } = locate(id, { product });
102
+ const item = updateWorkItem(project.dir, id, saidPlainly(patch, source), { epoch, source });
103
+ return { ...item, product: project.id, productName: project.name };
104
+ }
105
+
106
+ /*
107
+ * ONLY SHE CLOSES A ROW SHE WROTE, UNLESS SHE SAID TO (w-e372d22a99).
108
+ *
109
+ * Her words, 2026-09-27: "If I ask for something, its job is to return it back.
110
+ * Only when I request it, 'Close it,' or say, 'All right, I'm done with this,'
111
+ * or something like that, should it close it."
112
+ *
113
+ * Until then a worker answered her ask and set it done in the same call, and
114
+ * the row wore "Closed" from then on. On the row she reported, w-3384aa9e5c, a
115
+ * worker claimed at 19:45:20 and closed at 19:46:09. She could not find it, and
116
+ * found it in Closed. Measured across her store that night: 43 of her rows had
117
+ * been closed by an agent against 361 closed by her.
118
+ *
119
+ * So a `done` on a row labelled `founder` needs HER WORDS asking for it, quoted
120
+ * in `closeBecause` and found in what she wrote on the row. Without them the
121
+ * rest of the write lands and the row stays open: the answer reaches her inbox
122
+ * as an answer (`answeredHerAsk`, list-rules.ts), and `awaitingHer`
123
+ * (supervisor.mjs) keeps a second worker off it until she speaks. The quote is
124
+ * checked for being hers, not for meaning close; that judgement is the
125
+ * worker's, and the quote is what makes it one it has to make on purpose.
126
+ *
127
+ * A run of a repeating task is not her ask. She set the task up to run daily
128
+ * and close quietly when there is nothing to say, so `repeat:` rows keep that.
129
+ */
130
+ const plainly = (s) => String(s ?? '')
131
+ .toLowerCase()
132
+ .replace(/[‘’]/g, "'")
133
+ .replace(/[“”]/g, '"')
134
+ .replace(/\s+/g, ' ')
135
+ .trim();
136
+
137
+ export function herRow(item) {
138
+ const labels = item?.labels ?? [];
139
+ return labels.includes('founder') && !labels.some((l) => String(l).startsWith('repeat:'));
140
+ }
141
+
142
+ export function saidToClose(item, words) {
143
+ const quote = plainly(words).replace(/^["'\s]+|["'.,!?\s]+$/g, '');
144
+ if (quote.length < 4) return false;
145
+ return [item?.answer, item?.body, item?.title].some((t) => plainly(t).includes(quote));
146
+ }
147
+
148
+ const LEFT_OPEN = 'The row was left OPEN. The founder wrote it, and only she closes her own rows unless she asked you to: everything else you sent was written, and your answer reaches her inbox as an answer. If she did ask you to close it, send status done again with closeBecause set to her exact words asking for it.';
149
+
150
+ /**
151
+ * What a refused write is told about itself. The sentence matters as much as
152
+ * the file: the session that lost a night's work read "claim it first", had no
153
+ * reason to think anything had survived, and ended its turn.
154
+ */
155
+ function carried(parked) {
156
+ if (!parked) return ' Your write could NOT be parked, so re-send it once you hold the claim.';
157
+ return ` YOUR WRITE WAS NOT LOST: it is parked at ${parked.relPath} inside the product folder, and the next session to claim ${parked.item} is handed it. If this session should still be the one to say it, claim the item again and send it again.`;
158
+ }
159
+
160
+ /** What the session taking the row over is told at the moment it takes it. */
161
+ function pendingNotice(pending) {
162
+ const n = pending.length;
163
+ return `${n} write${n === 1 ? '' : 's'} to this row ${n === 1 ? 'was' : 'were'} refused when an earlier session lost its claim, and ${n === 1 ? 'it is' : 'they are'} waiting in \`pending\`. Nobody has seen ${n === 1 ? 'it' : 'them'}: not the founder, not the row. Read ${n === 1 ? 'it' : 'them'}, decide what is still true, and carry that into your own note or result. Your first write to this row files ${n === 1 ? 'it' : 'them'} as delivered.`;
164
+ }
165
+
166
+ /**
167
+ * A registry of the claims THIS server process holds, and the timer that keeps
168
+ * them alive. Purely in-memory, and that is correct: it describes this process's
169
+ * liveness, and it is supposed to vanish when the process does.
170
+ */
171
+ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {}) {
172
+ // `live` is the difference between holding the row and merely still being
173
+ // allowed to speak on it. A finished row gives its claim straight back, so
174
+ // nothing waits on a lease for work that is over, but the session keeps its
175
+ // epoch: the founder replies to finished rows constantly, and the session she
176
+ // is replying to has to be able to answer her.
177
+ const held = new Map(); // workItemId -> { dir, epoch, pending, live }
178
+ let timer = null;
179
+ const liveCount = () => [...held.values()].filter((h) => h.live).length;
180
+
181
+ const beat = () => {
182
+ for (const [id, { dir, epoch, live }] of held) {
183
+ if (!live) continue;
184
+ try {
185
+ heartbeatWorkItem(dir, id, { epoch, holder });
186
+ } catch (err) {
187
+ // A failed beat is not fatal: the lease still has two beats of margin,
188
+ // and the next one may well succeed. Losing the claim is the correct
189
+ // outcome if they all keep failing.
190
+ console.error(`${LOG} heartbeat failed for ${id}: ${err?.message ?? err}`);
191
+ }
192
+ }
193
+ };
194
+
195
+ const ensureTimer = () => {
196
+ if (timer || !liveCount()) return;
197
+ timer = setInterval(beat, heartbeatMs);
198
+ // Never keep the process alive just to beat: if the agent session is done,
199
+ // this server should exit, and the lapse is exactly the signal we want.
200
+ timer.unref?.();
201
+ };
202
+
203
+ const stopTimer = () => {
204
+ if (timer && !liveCount()) { clearInterval(timer); timer = null; }
205
+ };
206
+
207
+ // Park a refused write in the product that owns the row. The lookup can fail
208
+ // (an id that belongs to no product at all), and a failure to park must never
209
+ // hide the refusal itself, so it degrades to a null and the caller still
210
+ // throws.
211
+ const parkIn = (dir, id, patch, epoch, reason) => {
212
+ try {
213
+ return parkPendingWrite(dir, id, patch, { holder, epoch, reason });
214
+ } catch (err) {
215
+ console.error(`${LOG} could not park a refused write for ${id}: ${err?.message ?? err}`);
216
+ return null;
217
+ }
218
+ };
219
+
220
+ const park = (id, patch, reason) => {
221
+ let dir = null;
222
+ try { dir = locate(id).project.dir; } catch { return null; }
223
+ return parkIn(dir, id, patch, null, reason);
224
+ };
225
+
226
+ const registry = {
227
+ holder,
228
+ async claim({ product = null, id = null, filter = null }) {
229
+ // A claim by id can address any product; a filtered pull walks products in
230
+ // order until one yields, so a worker with nothing specific to do gets the
231
+ // first available item anywhere rather than nothing.
232
+ const projects = product ? [resolveProduct(product)] : (id ? [locate(id).project] : listProjects());
233
+
234
+ for (const p of projects) {
235
+ const result = await claimWorkItem(p.dir, { id, filter, holder });
236
+ if (result.claimed) {
237
+ // A write an earlier session had refused is handed over HERE, at the
238
+ // moment the row changes hands, because that is the only moment a
239
+ // session can be told something without having to remember to look.
240
+ const pending = readPendingWrites(p.dir, result.item.id);
241
+ held.set(result.item.id, { dir: p.dir, epoch: result.epoch, pending: pending.length > 0, live: true });
242
+ ensureTimer();
243
+ return {
244
+ ...result,
245
+ item: { ...result.item, product: p.id, productName: p.name },
246
+ ...(pending.length ? { pending, pendingNotice: pendingNotice(pending) } : {}),
247
+ };
248
+ }
249
+ // Asking for a specific id and being refused is an answer, not a reason
250
+ // to go looking in other products.
251
+ if (id) return result;
252
+ }
253
+ return { claimed: false, reason: 'nothing available in any product' };
254
+ },
255
+
256
+ /**
257
+ * Finish or update an item this process holds, under its own epoch.
258
+ *
259
+ * A refused write is PARKED rather than dropped. Both refusals are covered
260
+ * and they fail differently: not holding the claim throws here, while a
261
+ * claim that lapsed and was taken by somebody else does not throw at all —
262
+ * the line is appended and the fold quietly skips it, so the caller gets
263
+ * back an item that looks fine and is missing every word it just wrote.
264
+ * That second one is the quieter of the two and it reads the same to the
265
+ * founder: a message that was never there.
266
+ *
267
+ * NOT HOLDING THE ROW IS NOW A REASON TO TAKE IT, NOT A REASON TO REFUSE.
268
+ * The refusal was written for the case it almost never caught. Measured
269
+ * across the 119 writes parked on the founder's store since 24 August 2026:
270
+ * 99 were a session writing before it had claimed, and 12 were a session
271
+ * writing again after finishing the row, which drops the claim from this
272
+ * map. In all 111 the row was free to this session or already its own, and
273
+ * the message was parked anyway, so 211,573 characters of finished agent
274
+ * writing missed the row on the first attempt and the founder watched
275
+ * sessions sit out a lease to say something they had already written. So a
276
+ * write on a row we are not holding tries the claim first, and only parks
277
+ * when the claim is genuinely refused, which means somebody else is on it.
278
+ */
279
+ async update(id, patch) {
280
+ let holding = held.get(id);
281
+ if (!holding?.live) {
282
+ let taken = null;
283
+ try { taken = await registry.claim({ id }); } catch { /* an id in no product: park below */ }
284
+ const fresh = held.get(id);
285
+ if (fresh?.live) holding = fresh;
286
+ else if (!holding) {
287
+ // Nobody handed it over and we have no standing on it, so this is the
288
+ // refusal the fence exists for: somebody else is working the row.
289
+ const why = taken?.heldBy
290
+ ? `${id} is held by ${taken.heldBy}`
291
+ : `${holder} could not take ${id} (${taken?.reason ?? 'no such work item'})`;
292
+ const parked = park(id, patch, `${holder} was not holding ${id} when it wrote`);
293
+ throw new Error(`${why}, so this write could not be made.${carried(parked)}`);
294
+ }
295
+ // Otherwise the row was finished by this session and the claim is back
296
+ // on the shelf. We still own the epoch, so we may still speak, and the
297
+ // fence below still catches the case where somebody else took over.
298
+ }
299
+
300
+ const { closeBecause, ...asked } = patch;
301
+ let leftOpen = false;
302
+ if (asked.status === 'done') {
303
+ const before = readWorkItem(holding.dir, id);
304
+ if (herRow(before) && !saidToClose(before, closeBecause)) {
305
+ delete asked.status;
306
+ leftOpen = true;
307
+ }
308
+ }
309
+ patch = asked;
310
+ let item = Object.keys(patch).length
311
+ ? updateWorkItem(holding.dir, id, patch, { epoch: holding.epoch })
312
+ : readWorkItem(holding.dir, id);
313
+
314
+ // The fence. A claim epoch above ours means the row changed hands while
315
+ // we were working and the fold has just discarded the line we appended.
316
+ if (item && item.epoch > holding.epoch) {
317
+ held.delete(id);
318
+ stopTimer();
319
+ const parked = parkIn(holding.dir, id, patch, holding.epoch,
320
+ `claim lapsed: ${holder} wrote under epoch ${holding.epoch}, the row is now on epoch ${item.epoch}`);
321
+ throw new Error(`the claim on ${id} lapsed while this session worked and the row is now on epoch ${item.epoch}, so the store discarded this write.${carried(parked)}`);
322
+ }
323
+
324
+ // We were handed somebody else's message when we claimed, and we have now
325
+ // spoken on this row, so it is ours to have carried and must not be handed
326
+ // to a third session as if it were still waiting.
327
+ if (holding.pending) {
328
+ collectPendingWrites(holding.dir, id, { by: holder });
329
+ holding.pending = false;
330
+ }
331
+
332
+ // FINISHING A ROW GIVES THE CLAIM BACK NOW, not in five minutes. It used
333
+ // to drop the row from this map and leave the claim line standing, so the
334
+ // ledger went on reading "held by <this session>" for a full lease after
335
+ // the work was over: the founder's next reply could not be answered, and
336
+ // a session that tried was refused by its own name. The entry stays, and
337
+ // it is the epoch that keeps this session able to answer her.
338
+ if (item?.status === 'done' && holding.live) {
339
+ try { releaseWorkItem(holding.dir, id, { epoch: holding.epoch }); }
340
+ catch (err) { console.error(`${LOG} could not give back the claim on ${id}: ${err?.message ?? err}`); }
341
+ holding.live = false;
342
+ stopTimer();
343
+ }
344
+ // Left open, the claim still goes back now, for the same reason as a
345
+ // finish: the work is over and her reply must find the row free. The
346
+ // release is what puts the status back to open.
347
+ if (leftOpen) {
348
+ if (holding.live) {
349
+ try { item = releaseWorkItem(holding.dir, id, { epoch: holding.epoch }); }
350
+ catch (err) { console.error(`${LOG} could not give back the claim on ${id}: ${err?.message ?? err}`); }
351
+ holding.live = false;
352
+ stopTimer();
353
+ }
354
+ return { ...item, leftOpen: LEFT_OPEN };
355
+ }
356
+ return item;
357
+ },
358
+
359
+ release(id) {
360
+ const holding = held.get(id);
361
+ if (!holding?.live) throw new Error(`this session does not hold ${id}`);
362
+ const item = releaseWorkItem(holding.dir, id, { epoch: holding.epoch });
363
+ held.delete(id);
364
+ stopTimer();
365
+ return item;
366
+ },
367
+
368
+ holding: () => [...held.entries()].filter(([, h]) => h.live).map(([id]) => id),
369
+ // Test seams; not part of normal use.
370
+ _beat: beat,
371
+ _stop: () => { if (timer) clearInterval(timer); timer = null; held.clear(); },
372
+ };
373
+
374
+ return registry;
375
+ }
376
+
377
+ export { LEASE_MS, HEARTBEAT_MS };
package/mcp/stdio.mjs ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ // The stdio transport. Deliberately the thinnest file in the build: everything
3
+ // it knows is how to hand mcp/tools.mjs to a pipe, so the day the app wants HTTP
4
+ // instead, the sibling file that does it changes nothing else.
5
+ //
6
+ // Run it directly: STORE_ACCOUNT_ID=<id> node mcp/stdio.mjs
7
+ // Register it: claude mcp add <name> -- node /path/to/mcp/stdio.mjs
8
+ //
9
+ // THE SERVER IS NAMED FROM shared/product-name.mjs AND NOWHERE ELSE, which is
10
+ // the same rule the rest of the app already follows. It used to be a literal
11
+ // string, because this server was lifted out of another product, and that
12
+ // product's name reached every worker as its tool prefix and every brief that
13
+ // had to name a tool.
14
+ //
15
+ // One rule this file exists to enforce: STDOUT BELONGS TO THE PROTOCOL. A stray
16
+ // console.log anywhere in the server corrupts the JSON-RPC stream and the client
17
+ // sees a parse error rather than whatever was printed, so logging goes to
18
+ // stderr, and console.log is rebound below so that a careless one downstream
19
+ // cannot take the server down.
20
+
21
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
+ import { buildTools } from './tools.mjs';
24
+ import { NAME, Name, nameSlug } from '../shared/product-name.mjs';
25
+
26
+ const LOG = `[${nameSlug}-store]`;
27
+
28
+ console.log = (...args) => console.error(LOG, ...args);
29
+
30
+ async function main() {
31
+ const { account, tools, claims } = buildTools();
32
+
33
+ const server = new McpServer(
34
+ { name: nameSlug, version: '1.0.0' },
35
+ {
36
+ instructions: [
37
+ `${Name} is this founder's product store: several products at once, each with its documents, personas, transcripts, reports, and designs, plus the skills describing how that work is done well.`,
38
+ 'It holds the thinking, not the software. Product code lives in ordinary git checkouts (each product\'s repoPath), which you work in with your own tools.',
39
+ 'Several sessions may be running against this store at the same time, so claim a work item before acting on it.',
40
+ ].join(' '),
41
+ },
42
+ );
43
+
44
+ for (const tool of tools) {
45
+ server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.run);
46
+ }
47
+
48
+ // Give held work items back promptly on a clean exit instead of making the
49
+ // next worker wait out a full lease. Best effort by design: the lease is the
50
+ // guarantee, and this is only the courtesy that makes the common case fast.
51
+ const goodbye = () => {
52
+ for (const id of claims.holding()) {
53
+ try { claims.release(id); } catch { /* the lease will handle it */ }
54
+ }
55
+ process.exit(0);
56
+ };
57
+ process.on('SIGINT', goodbye);
58
+ process.on('SIGTERM', goodbye);
59
+
60
+ await server.connect(new StdioServerTransport());
61
+ console.error(`${LOG} serving account ${account.accountId} from ${account.root} (${tools.length} tools) for ${NAME}`);
62
+ }
63
+
64
+ main().catch((err) => {
65
+ // A startup failure is almost always configuration (no account, wrong
66
+ // STORE_HOME), so it must be readable on stderr rather than a stack trace
67
+ // the client swallows.
68
+ console.error(`${LOG} failed to start: ${err?.message ?? err}`);
69
+ process.exit(1);
70
+ });