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.
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- 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
|
+
});
|