agentbox-app 0.1.13 → 0.1.16
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/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// The store tools a teammate uses on its runs: talk in the team chat, pass work
|
|
2
|
+
// to a teammate, keep what it learns about the person, and set itself up.
|
|
3
|
+
//
|
|
4
|
+
// Which teammate the session is comes from the app, not the model: the run it
|
|
5
|
+
// was started for (`ZERO_ITEM`) carries `teammate:<id>`, so no argument names
|
|
6
|
+
// who is speaking and no session can speak as another teammate.
|
|
7
|
+
import { readWorkItem } from '../../main/store/work-items.mjs';
|
|
8
|
+
import { listProjects, legacyRoot } from '../../main/store/project.mjs';
|
|
9
|
+
import {
|
|
10
|
+
teamDir, readTeammates, readChat, writeTeammate, writeChat, newMemoryId,
|
|
11
|
+
readPlaybooks, writePlaybook, newPlaybookId, newLessonId, newDutyId,
|
|
12
|
+
} from '../../main/store/teammates-files.mjs';
|
|
13
|
+
import { teammateIdOf, hopOf, mentionsIn, isWorks, letterFor, cleanDuty, dutyStart, YOU } from '../../shared/teammates.mjs';
|
|
14
|
+
import { periodKey } from '../../shared/repeats.mjs';
|
|
15
|
+
import { resolveProduct, resolveAccount } from './account.mjs';
|
|
16
|
+
|
|
17
|
+
// A title or a choice is drawn as plain words, so markdown in one shows as stars.
|
|
18
|
+
const plain = (s) => String(s ?? '').replace(/\*\*|__|`/g, '').replace(/\s+/g, ' ').trim();
|
|
19
|
+
|
|
20
|
+
const NOT_A_TEAMMATE = 'this session is not a teammate\'s run, so it has no team chat to write to';
|
|
21
|
+
|
|
22
|
+
function projectOf(itemId, product) {
|
|
23
|
+
if (product) return resolveProduct(product);
|
|
24
|
+
for (const p of listProjects()) {
|
|
25
|
+
try { if (readWorkItem(p.dir, itemId)) return p; } catch { /* a product with no ledger */ }
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`no thread ${itemId} in any product`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `outsider` lets a thread that is no teammate's run in: it speaks for the
|
|
31
|
+
// person (`me` is null), which is what they asked it to do.
|
|
32
|
+
export function teammateContext({ env = process.env, now = Date.now(), outsider = false } = {}) {
|
|
33
|
+
const itemId = String(env.ZERO_ITEM ?? '').trim();
|
|
34
|
+
if (!itemId) throw new Error(NOT_A_TEAMMATE);
|
|
35
|
+
const project = projectOf(itemId, String(env.ZERO_PRODUCT ?? '').trim() || null);
|
|
36
|
+
const item = readWorkItem(project.dir, itemId, now);
|
|
37
|
+
if (!item) throw new Error(`no thread ${itemId} in ${project.name}`);
|
|
38
|
+
const tid = teammateIdOf(item);
|
|
39
|
+
if (!tid && !outsider) throw new Error(NOT_A_TEAMMATE);
|
|
40
|
+
const accountId = String(env.STORE_ACCOUNT_ID ?? '').trim() || resolveAccount().accountId;
|
|
41
|
+
const dir = teamDir(legacyRoot(), accountId);
|
|
42
|
+
const team = readTeammates(dir, now);
|
|
43
|
+
if (!tid) return { dir, me: null, team, item, project, hop: 0, via: { via: item.id, viaProduct: project.id } };
|
|
44
|
+
const me = team.find((t) => t.id === tid);
|
|
45
|
+
if (!me) throw new Error(`the teammate behind ${itemId} is no longer on the team`);
|
|
46
|
+
return { dir, me, team, item, project, hop: hopOf(item), via: {} };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function idsFor(names, team, me) {
|
|
50
|
+
const list = (Array.isArray(names) ? names : [names]).map((n) => String(n ?? '').trim().replace(/^@/, '')).filter(Boolean);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const n of list) {
|
|
53
|
+
const lower = n.toLowerCase();
|
|
54
|
+
const t = team.find((x) => x.id === n || x.name.toLowerCase() === lower);
|
|
55
|
+
if (!t) throw new Error(`no teammate called ${n}. The team: ${team.map((x) => x.name).join(', ')}`);
|
|
56
|
+
if (t.id !== me?.id) out.push(t.id);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function teamPost({ text, to = [], options } = {}, ctx = {}) {
|
|
62
|
+
const { dir, me, team, hop, via } = teammateContext({ ...ctx, outsider: true });
|
|
63
|
+
const words = String(text ?? '').trim();
|
|
64
|
+
if (!words) throw new Error('nothing to post');
|
|
65
|
+
const ids = new Set([...idsFor(to, team, me), ...mentionsIn(words, team).filter((x) => x !== me?.id)]);
|
|
66
|
+
const opts = me && Array.isArray(options) ? options.map(plain).filter(Boolean).slice(0, 4) : null;
|
|
67
|
+
const m = writeChat(dir, { kind: 'msg', from: me?.id ?? YOU, to: [...ids], text: words.slice(0, 4000), hop, ...via, ...(opts?.length ? { options: opts } : {}) }, ctx.now);
|
|
68
|
+
return { posted: m.id, to: [...ids].map((x) => team.find((t) => t.id === x)?.name), note: ids.size ? 'The teammates named will wake to read it.' : 'Posted. Nobody was named, so nobody is woken by it.' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function teamPass({ to, title, details, project: named } = {}, ctx = {}) {
|
|
72
|
+
const { dir, me, team, hop, via, project } = teammateContext({ ...ctx, outsider: true });
|
|
73
|
+
// The work is done where it is named, or for a thread handing it on, in that
|
|
74
|
+
// thread's project; otherwise in the receiver's own.
|
|
75
|
+
const where = named ? resolveProduct(named).id : me ? null : project.id;
|
|
76
|
+
const [target] = idsFor(to, team, me);
|
|
77
|
+
if (!target) throw new Error('pass work to another teammate, not to yourself');
|
|
78
|
+
const what = plain(title);
|
|
79
|
+
if (!what) throw new Error('give the work a short title');
|
|
80
|
+
const m = writeChat(dir, { kind: 'pass', from: me?.id ?? YOU, to: target, title: what.slice(0, 200), details: String(details ?? '').slice(0, 6000), hop, ...via, ...(where ? { product: where } : {}) }, ctx.now);
|
|
81
|
+
return { passed: m.id, to: team.find((t) => t.id === target)?.name };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function teamRemember({ text } = {}, ctx = {}) {
|
|
85
|
+
const { dir, me, item } = teammateContext(ctx);
|
|
86
|
+
const words = String(text ?? '').trim();
|
|
87
|
+
if (!words) throw new Error('nothing to remember');
|
|
88
|
+
if (me.memory.some((m) => m.text.toLowerCase() === words.toLowerCase())) return { remembered: false, note: 'already kept' };
|
|
89
|
+
const now = ctx.now ?? Date.now();
|
|
90
|
+
const mem = { id: newMemoryId(), text: words.slice(0, 300), from: `run ${item.id}`, at: now };
|
|
91
|
+
writeTeammate(dir, me.id, { remember: mem }, now);
|
|
92
|
+
writeChat(dir, { kind: 'act', from: me.id, verb: 'learned', text: mem.text, ref: mem.id }, now);
|
|
93
|
+
return { remembered: true, id: mem.id };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const EVERY = ['day', 'weekday', 'week', 'never'];
|
|
97
|
+
|
|
98
|
+
// Or answered in the setup thread itself, which reaches the run as its answer.
|
|
99
|
+
function answeredInThread(item) {
|
|
100
|
+
const said = typeof item?.answer === 'string' ? item.answer.trim() : '';
|
|
101
|
+
return !!said && said !== '(withdrawn)' && item.wrote?.answer?.source === 'founder';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function answeredSince(dir, id) {
|
|
105
|
+
const { messages } = readChat(dir);
|
|
106
|
+
const asked = messages.filter((m) => m.kind === 'msg' && m.from === YOU && m.setup && (m.to ?? []).includes(id)).at(-1);
|
|
107
|
+
return messages.some((m) => m.kind === 'msg' && m.from === YOU && !m.setup && (m.to ?? []).includes(id) && m.ts > (asked?.ts ?? 0));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// What a change to these says in the chat, so you can see it saved.
|
|
111
|
+
const SAID = {
|
|
112
|
+
name: 'its name', role: 'its role', job: 'its job', goal: 'its goal', counted: 'how it counts its goal',
|
|
113
|
+
asksFirst: 'what needs your yes first', works: 'when it works', duties: 'what it does on its own',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Duties keep their id, and what they have served, when one with the same
|
|
117
|
+
// words is set again.
|
|
118
|
+
function dutiesFrom(list, me, now) {
|
|
119
|
+
if (!Array.isArray(list)) throw new Error('duties is a list of { what, every, at, on } or { what, after: "ship", most }');
|
|
120
|
+
const out = [];
|
|
121
|
+
const served = {};
|
|
122
|
+
for (const raw of list) {
|
|
123
|
+
const what = String(raw?.what ?? '').trim();
|
|
124
|
+
if (!what) throw new Error('each duty needs what it does');
|
|
125
|
+
const had = (me.duties ?? []).find((d) => d.what.toLowerCase() === what.toLowerCase());
|
|
126
|
+
const d = cleanDuty({ ...raw, what, id: had?.id ?? newDutyId(), at: raw?.after === 'ship' ? undefined : (Array.isArray(raw?.at) ? raw.at : raw?.at ? [raw.at] : []) });
|
|
127
|
+
if (!d) throw new Error(`"${what}" cannot run as written: every is day, weekday or week, at is a list of HH:MM times in 24 hours, on is 0 (Sunday) to 6 for weekly, or after is "ship" with most runs a week`);
|
|
128
|
+
out.push(d);
|
|
129
|
+
if (!had) served[d.id] = dutyStart(d, now);
|
|
130
|
+
}
|
|
131
|
+
return { duties: out, served };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function teamSetup({ name, role, job, goal, counted, asksFirst, every, at, on, duties, runsOn, ready } = {}, ctx = {}) {
|
|
135
|
+
const { dir, me, team, item } = teammateContext(ctx);
|
|
136
|
+
const now = ctx.now ?? Date.now();
|
|
137
|
+
if (typeof asksFirst === 'string' && /\?\s*$/.test(asksFirst)) {
|
|
138
|
+
throw new Error('asksFirst is what needs the person\'s yes before you do it, said as a statement ("Anything that goes out in public"). Ask a question with team_post instead.');
|
|
139
|
+
}
|
|
140
|
+
const patch = {};
|
|
141
|
+
if (typeof name === 'string' && name.trim()) {
|
|
142
|
+
const clean = name.trim().replace(/\s+/g, ' ').slice(0, 40);
|
|
143
|
+
if (team.some((t) => t.id !== me.id && t.name.toLowerCase() === clean.toLowerCase())) throw new Error(`${clean} is taken; pick another name`);
|
|
144
|
+
patch.name = clean;
|
|
145
|
+
patch.letter = letterFor(clean, team.filter((t) => t.id !== me.id));
|
|
146
|
+
}
|
|
147
|
+
for (const [k, v] of Object.entries({ role, job, goal, counted, asksFirst })) if (typeof v === 'string' && v.trim()) patch[k] = v.trim();
|
|
148
|
+
if (every !== undefined) {
|
|
149
|
+
if (!EVERY.includes(every)) throw new Error(`every is one of ${EVERY.join(', ')}`);
|
|
150
|
+
if (every === 'never') patch.works = null;
|
|
151
|
+
else {
|
|
152
|
+
const works = { every, at: at ?? '09:00', ...(every === 'week' ? { on: Number.isInteger(on) ? on : 1 } : {}) };
|
|
153
|
+
if (!isWorks(works)) throw new Error('at is HH:MM in 24 hours, and on is 0 (Sunday) to 6 for a weekly round');
|
|
154
|
+
patch.works = works;
|
|
155
|
+
patch.served = periodKey(works, now) ?? '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (duties !== undefined) {
|
|
159
|
+
const made = dutiesFrom(duties, me, now);
|
|
160
|
+
patch.duties = made.duties;
|
|
161
|
+
patch.works = null;
|
|
162
|
+
if (Object.keys(made.served).length) patch.dutyServed = made.served;
|
|
163
|
+
}
|
|
164
|
+
if (runsOn !== undefined) {
|
|
165
|
+
if (runsOn !== 'claude' && runsOn !== 'codex') throw new Error('runsOn is claude or codex');
|
|
166
|
+
patch.engine = runsOn;
|
|
167
|
+
}
|
|
168
|
+
// A new teammate asks before it starts, so ready waits for the person's
|
|
169
|
+
// own words to it after the ask that made it.
|
|
170
|
+
if (ready === true && me.status === 'draft' && !answeredSince(dir, me.id) && !answeredInThread(item)) {
|
|
171
|
+
throw new Error('ask you first: post your questions with team_post, each with options, and finish; you can go ready once the person has answered');
|
|
172
|
+
}
|
|
173
|
+
if (ready === true) patch.status = 'active';
|
|
174
|
+
writeTeammate(dir, me.id, patch, now);
|
|
175
|
+
if (ready === true && me.status === 'draft') writeChat(dir, { kind: 'act', from: me.id, verb: 'started' }, now);
|
|
176
|
+
const changed = Object.keys(SAID).filter((k) => k in patch && JSON.stringify(patch[k]) !== JSON.stringify(me[k] ?? null));
|
|
177
|
+
if (me.status === 'active' && changed.length) {
|
|
178
|
+
const parts = changed.map((k) => SAID[k]);
|
|
179
|
+
writeChat(dir, { kind: 'act', from: me.id, verb: 'updated', text: `Updated ${parts.length > 1 ? `${parts.slice(0, -1).join(', ')} and ${parts.at(-1)}` : parts[0]}` }, now);
|
|
180
|
+
}
|
|
181
|
+
const after = readTeammates(dir, now).find((t) => t.id === me.id);
|
|
182
|
+
return { teammate: { name: after.name, role: after.role, job: after.job, goal: after.goal, counted: after.counted, asksFirst: after.asksFirst, works: after.works, duties: after.duties, runsOn: after.engine ?? 'your default', status: after.status } };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// A lesson about how the person likes a kind of work done, kept in the
|
|
186
|
+
// playbook of that title so every teammate doing that work reads it. With no
|
|
187
|
+
// lesson it starts reading a playbook another teammate wrote.
|
|
188
|
+
export function teamPlaybook({ playbook, lesson } = {}, ctx = {}) {
|
|
189
|
+
const { dir, me, item, project } = teammateContext(ctx);
|
|
190
|
+
const now = ctx.now ?? Date.now();
|
|
191
|
+
const title = plain(playbook).slice(0, 120);
|
|
192
|
+
if (!title) throw new Error('name the playbook, like "How you write posts"');
|
|
193
|
+
const books = readPlaybooks(dir);
|
|
194
|
+
let book = books.find((b) => b.title.toLowerCase() === title.toLowerCase());
|
|
195
|
+
if (lesson === undefined) {
|
|
196
|
+
if (!book) throw new Error(`no playbook called ${title}. The team has: ${books.map((b) => b.title).join(', ') || 'none yet'}. Add a lesson to start one.`);
|
|
197
|
+
if (!book.readers.includes(me.id)) writePlaybook(dir, { kind: 'reader', book: book.id, teammate: me.id }, now);
|
|
198
|
+
return { playbook: book.id, reading: true, lessons: book.lessons.map((l) => l.text) };
|
|
199
|
+
}
|
|
200
|
+
const text = String(lesson ?? '').trim().slice(0, 400);
|
|
201
|
+
if (!text) throw new Error('the lesson is empty');
|
|
202
|
+
if (!book) {
|
|
203
|
+
book = { id: newPlaybookId(), title, lessons: [], readers: [] };
|
|
204
|
+
writePlaybook(dir, { kind: 'book', id: book.id, title }, now);
|
|
205
|
+
}
|
|
206
|
+
if (book.lessons.some((l) => l.text.toLowerCase() === text.toLowerCase())) return { playbook: book.id, kept: false, note: 'already in it' };
|
|
207
|
+
const id = newLessonId();
|
|
208
|
+
writePlaybook(dir, { kind: 'lesson', id, book: book.id, text, by: me.id, from: { product: project.id, id: item.id, title: plain(item.label || item.title) } }, now);
|
|
209
|
+
writeChat(dir, { kind: 'act', from: me.id, verb: 'lesson', ref: book.id, text }, now);
|
|
210
|
+
return { playbook: book.id, kept: true, id };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function teamRoster(ctx = {}) {
|
|
214
|
+
const { dir, me, team } = teammateContext({ ...ctx, outsider: true });
|
|
215
|
+
const { messages } = readChat(dir);
|
|
216
|
+
const nameOf = (id) => (id === YOU ? 'the person' : team.find((t) => t.id === id)?.name ?? 'a teammate');
|
|
217
|
+
return {
|
|
218
|
+
you: me?.name ?? null,
|
|
219
|
+
team: team.map((t) => ({ name: t.name, role: t.role, job: t.job, paused: t.paused, status: t.status })),
|
|
220
|
+
recent: messages.filter((m) => m.kind === 'msg' || m.kind === 'pass').slice(-20).map((m) => ({ from: nameOf(m.from), to: m.kind === 'pass' ? [nameOf(m.to)] : (m.to ?? []).map(nameOf), text: m.kind === 'pass' ? `passed: ${m.title}` : m.text, at: new Date(m.ts).toISOString() })),
|
|
221
|
+
};
|
|
222
|
+
}
|
package/mcp/core/work.mjs
CHANGED
|
@@ -96,10 +96,63 @@ export function createItem(product, fields = {}, { source = 'agent', from = [] }
|
|
|
96
96
|
const p = resolveProduct(product);
|
|
97
97
|
const sources = [...from, fields.parent].filter(Boolean).map((id) => { try { return locate(id).item; } catch { return null; } });
|
|
98
98
|
const kept = fields.visibility ? {} : privacyFrom(sources);
|
|
99
|
+
// A row filed under a thread runs as that thread does, or it falls to the default engine.
|
|
100
|
+
const parent = fields.parent ? sources.at(-1) : null;
|
|
101
|
+
for (const f of ['engine', 'model', 'effort']) if (fields[f] == null && parent?.[f]) kept[f] = parent[f];
|
|
99
102
|
const item = createWorkItem(p.dir, saidPlainly({ ...fields, ...kept }, source), { source });
|
|
100
103
|
return { ...item, product: p.id, productName: p.name };
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/*
|
|
107
|
+
* ONE JOB IS ONE THREAD, AND A RUN THAT FILES A SECOND ONE IS TOLD SO
|
|
108
|
+
* (w-31b2eb82ba).
|
|
109
|
+
*
|
|
110
|
+
* "I want you to fix all of those in one thread, feels unecessary to create
|
|
111
|
+
* multiple threads and a bit confusing. I feel like my inbox has been getting
|
|
112
|
+
* super cluttered with agents separating into multiple threads when I don't
|
|
113
|
+
* want them to."
|
|
114
|
+
*
|
|
115
|
+
* A QA campaign had found four things and had ONE question in it — do you want
|
|
116
|
+
* these fixed — and filed four threads, which is four things to read, rank and
|
|
117
|
+
* either start or close that did not exist until it filed them. The brief is
|
|
118
|
+
* rewritten (briefs/worker.md, "One job is one thread") and the brief alone is
|
|
119
|
+
* not enough: all four went in from one session in 53 seconds at the end of a
|
|
120
|
+
* long run, and nothing anywhere counted them or pushed back.
|
|
121
|
+
*
|
|
122
|
+
* So the filing tool answers with the count. It does NOT refuse, because a
|
|
123
|
+
* refusal at this moment loses a finding an agent has already measured, and
|
|
124
|
+
* the judgement it is asking for is one only the agent can make. It is said
|
|
125
|
+
* where the session reads it while it can still fold the thread back.
|
|
126
|
+
*
|
|
127
|
+
* The group is the parent, because that is the question being asked: how many
|
|
128
|
+
* live threads now hang off the one conversation. A finished sibling is not
|
|
129
|
+
* counted; nobody is waiting on it. A thread filed under NOTHING is grouped by
|
|
130
|
+
* the run instead and says so in harder words, because a thread with no parent
|
|
131
|
+
* is not a proposal waiting in Later: it is a row in their inbox
|
|
132
|
+
* (`isProposal`, renderer/src/list-rules.ts).
|
|
133
|
+
*/
|
|
134
|
+
function alsoOnThisThread(beside, parent) {
|
|
135
|
+
const n = beside.length + 1;
|
|
136
|
+
const where = parent ? `under ${parent}` : 'under no thread at all';
|
|
137
|
+
const fold = parent
|
|
138
|
+
? `fold it into ${parent}`
|
|
139
|
+
: 'put it in the thread you are already on, under what is left to do';
|
|
140
|
+
return `${n} live threads are now filed ${where}, counting this one (${beside.join(', ')}). `
|
|
141
|
+
+ 'ONE JOB IS ONE THREAD: if they would answer all of these in one breath, they are one thread, so '
|
|
142
|
+
+ `${fold} and close this one with a note saying where the work went. Keep it separate only if two `
|
|
143
|
+
+ 'agents could genuinely run these at the same time, and then say in your answer why there is more '
|
|
144
|
+
+ 'than one.';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The live threads already filed under the same parent, this one aside. */
|
|
148
|
+
function besideIt(product, item) {
|
|
149
|
+
let items = [];
|
|
150
|
+
try { items = readWorkItems(resolveProduct(product).dir, Date.now()); } catch { return []; }
|
|
151
|
+
return items
|
|
152
|
+
.filter((i) => i.id !== item.id && i.parent === item.parent && i.status !== 'done')
|
|
153
|
+
.map((i) => i.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
/*
|
|
104
157
|
* A TASK AN AGENT FILES IS SEEN BY NO MORE PEOPLE THAN THE TASKS IT CAME FROM
|
|
105
158
|
* (w-e053ed3581). The agent asked into a private chat holds a task seen only
|
|
@@ -121,9 +174,21 @@ export function privacyFrom(items) {
|
|
|
121
174
|
return people.length ? { visibility: 'people', visibleTo: people } : { visibility: 'private' };
|
|
122
175
|
}
|
|
123
176
|
|
|
177
|
+
// A teammate's run is known by these labels, and labels are written whole, so
|
|
178
|
+
// a run that closes with ["clean"] would stop being its teammate's run.
|
|
179
|
+
const RUN_LABELS = ['teammate:', 'hop:', 'from:'];
|
|
180
|
+
function keepRunLabels(had, patch) {
|
|
181
|
+
if (!Array.isArray(patch.labels)) return patch;
|
|
182
|
+
const before = had?.labels ?? [];
|
|
183
|
+
if (!before.some((l) => String(l).startsWith('teammate:'))) return patch;
|
|
184
|
+
const kept = before.filter((l) => l === 'founder' || RUN_LABELS.some((p) => String(l).startsWith(p)));
|
|
185
|
+
const missing = kept.filter((l) => !patch.labels.includes(l));
|
|
186
|
+
return missing.length ? { ...patch, labels: [...patch.labels, ...missing] } : patch;
|
|
187
|
+
}
|
|
188
|
+
|
|
124
189
|
export function updateItem(id, patch, { product = null, epoch = null, source = 'agent' } = {}) {
|
|
125
|
-
const { project } = locate(id, { product });
|
|
126
|
-
const item = updateWorkItem(project.dir, id, saidPlainly(patch, source), { epoch, source });
|
|
190
|
+
const { project, item: had } = locate(id, { product });
|
|
191
|
+
const item = updateWorkItem(project.dir, id, saidPlainly(keepRunLabels(had, patch), source), { epoch, source });
|
|
127
192
|
return { ...item, product: project.id, productName: project.name };
|
|
128
193
|
}
|
|
129
194
|
|
|
@@ -158,7 +223,7 @@ const plainly = (s) => String(s ?? '')
|
|
|
158
223
|
|
|
159
224
|
export function herRow(item) {
|
|
160
225
|
const labels = item?.labels ?? [];
|
|
161
|
-
return labels.includes('founder') && !labels.some((l) => String(l).startsWith('repeat:'));
|
|
226
|
+
return labels.includes('founder') && !labels.some((l) => String(l).startsWith('repeat:') || String(l).startsWith('teammate:'));
|
|
162
227
|
}
|
|
163
228
|
|
|
164
229
|
export function saidToClose(item, words) {
|
|
@@ -244,6 +309,9 @@ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {})
|
|
|
244
309
|
// epoch: the founder replies to finished rows constantly, and the session she
|
|
245
310
|
// is replying to has to be able to answer her.
|
|
246
311
|
const held = new Map(); // workItemId -> { dir, epoch, pending, live }
|
|
312
|
+
// The threads this run has filed under no parent at all, so that the second
|
|
313
|
+
// loose one is counted even though the store cannot group them.
|
|
314
|
+
const loose = [];
|
|
247
315
|
let timer = null;
|
|
248
316
|
const liveCount = () => [...held.values()].filter((h) => h.live).length;
|
|
249
317
|
|
|
@@ -458,8 +526,12 @@ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {})
|
|
|
458
526
|
|
|
459
527
|
// Filing from this session: what it files is kept as private as every row
|
|
460
528
|
// it has held, finished ones too, since it may still be answering on them.
|
|
529
|
+
// And it is counted: `alsoOnThisThread` above says why.
|
|
461
530
|
file(product, fields) {
|
|
462
|
-
|
|
531
|
+
const item = createItem(product, fields, { from: [...held.keys()] });
|
|
532
|
+
const beside = item.parent ? besideIt(product, item) : [...loose];
|
|
533
|
+
if (!item.parent) loose.push(item.id);
|
|
534
|
+
return beside.length ? { ...item, alsoFiled: alsoOnThisThread(beside, item.parent) } : item;
|
|
463
535
|
},
|
|
464
536
|
|
|
465
537
|
holding: () => [...held.entries()].filter(([, h]) => h.live).map(([id]) => id),
|
package/mcp/stdio.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
22
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
+
import path from 'node:path';
|
|
23
24
|
import { buildTools } from './tools.mjs';
|
|
24
25
|
import { NAME, Name, nameSlug } from '../shared/product-name.mjs';
|
|
25
26
|
|
|
@@ -27,8 +28,13 @@ const LOG = `[${nameSlug}-store]`;
|
|
|
27
28
|
|
|
28
29
|
console.log = (...args) => console.error(LOG, ...args);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* One store server over `input` and `output`: this process's own stdio, or one
|
|
33
|
+
* cloud run's connection when many share a process (main/cloud/run.mjs), each
|
|
34
|
+
* holding its own claims (`holder`). Returns a close that gives them back.
|
|
35
|
+
*/
|
|
36
|
+
export async function serveStore({ input = process.stdin, output = process.stdout, holder, ownItem } = {}) {
|
|
37
|
+
const { account, tools, claims } = buildTools({ holder, ...(ownItem !== undefined ? { ownItem } : {}) });
|
|
32
38
|
|
|
33
39
|
const server = new McpServer(
|
|
34
40
|
{ name: nameSlug, version: '1.0.0' },
|
|
@@ -45,23 +51,30 @@ async function main() {
|
|
|
45
51
|
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.run);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
// Give held work items back promptly
|
|
49
|
-
// next worker wait out a full lease. Best effort by design: the lease is
|
|
50
|
-
// guarantee, and this is only the courtesy that makes the common case fast.
|
|
51
|
-
const
|
|
54
|
+
// Give held work items back promptly when the session ends instead of making
|
|
55
|
+
// the next worker wait out a full lease. Best effort by design: the lease is
|
|
56
|
+
// the guarantee, and this is only the courtesy that makes the common case fast.
|
|
57
|
+
const giveBack = () => {
|
|
52
58
|
for (const id of claims.holding()) {
|
|
53
59
|
try { claims.release(id); } catch { /* the lease will handle it */ }
|
|
54
60
|
}
|
|
55
|
-
process.exit(0);
|
|
56
61
|
};
|
|
57
|
-
process.on('SIGINT', goodbye);
|
|
58
|
-
process.on('SIGTERM', goodbye);
|
|
59
62
|
|
|
60
|
-
await server.connect(new StdioServerTransport());
|
|
63
|
+
await server.connect(new StdioServerTransport(input, output));
|
|
61
64
|
console.error(`${LOG} serving account ${account.accountId} from ${account.root} (${tools.length} tools) for ${NAME}`);
|
|
65
|
+
return { close: async () => { giveBack(); await server.close().catch(() => {}); } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const { close } = await serveStore();
|
|
70
|
+
const goodbye = () => { close().finally(() => process.exit(0)); };
|
|
71
|
+
process.on('SIGINT', goodbye);
|
|
72
|
+
process.on('SIGTERM', goodbye);
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
// Started as a program (mcp/agentbox-mcp.sh), not imported by a cloud run's relay.
|
|
76
|
+
const startedAsProgram = path.basename(process.argv[1] || '') === 'stdio.mjs';
|
|
77
|
+
if (startedAsProgram) main().catch((err) => {
|
|
65
78
|
// A startup failure is almost always configuration (no account, wrong
|
|
66
79
|
// STORE_HOME), so it must be readable on stderr rather than a stack trace
|
|
67
80
|
// the client swallows.
|
package/mcp/tools.mjs
CHANGED
|
@@ -17,6 +17,10 @@ import { listProducts, getProduct, createProduct, updateProduct } from './core/p
|
|
|
17
17
|
import { listDocuments, readDocument, writeDocument } from './core/documents.mjs';
|
|
18
18
|
import { listSkills, fetchSkill, fetchReference } from './core/skills.mjs';
|
|
19
19
|
import { listWorkItems, updateItem, createClaimRegistry } from './core/work.mjs';
|
|
20
|
+
import { readStandingInstruction, setStandingInstruction } from './core/repeats.mjs';
|
|
21
|
+
import { teamPost, teamPass, teamRemember, teamSetup, teamRoster, teamPlaybook } from './core/teammates.mjs';
|
|
22
|
+
import { readTeammates, teamDir } from '../main/store/teammates-files.mjs';
|
|
23
|
+
import { legacyRoot } from '../main/store/project.mjs';
|
|
20
24
|
import { listProductChats, readProductChat } from './core/chat.mjs';
|
|
21
25
|
import { lookAtPage } from './core/page.mjs';
|
|
22
26
|
import { WORK_ITEM_STATUSES } from '../shared/work-items.mjs';
|
|
@@ -33,6 +37,8 @@ import { Name, possessive } from '../shared/product-name.mjs';
|
|
|
33
37
|
const CAPABILITIES = [
|
|
34
38
|
'products', 'documents (read and write, any path in the product)',
|
|
35
39
|
'personas and transcripts (they are documents)', 'skills', 'work items', 'past chats (read only)',
|
|
40
|
+
'the standing instruction behind a repeating task, on a thread that is one of its runs',
|
|
41
|
+
'the team chat and passing work to a teammate, from any thread; remembering and setting up, on a thread that is a teammate\'s run',
|
|
36
42
|
'looking at a page running on this machine (look_at_page, local addresses only)',
|
|
37
43
|
];
|
|
38
44
|
const MISSING = [
|
|
@@ -50,9 +56,12 @@ const guard = (fn) => async (args) => {
|
|
|
50
56
|
try { return await fn(args ?? {}); } catch (err) { return { ...text(`error: ${err?.message ?? err}`), isError: true }; }
|
|
51
57
|
};
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
// `ownItem` is the row this server's session was started for, if any.
|
|
60
|
+
export function buildTools({ holder, ownItem = process.env.ZERO_ITEM || null } = {}) {
|
|
54
61
|
const account = resolveAccount();
|
|
55
62
|
const claims = createClaimRegistry({ holder: holder ?? `mcp-${process.pid}` });
|
|
63
|
+
const hasTeam = teammatesExist(String(process.env.STORE_ACCOUNT_ID ?? '').trim() || account.accountId);
|
|
64
|
+
const capabilities = hasTeam ? CAPABILITIES : CAPABILITIES.filter((c) => !c.includes('teammate'));
|
|
56
65
|
|
|
57
66
|
const tools = [
|
|
58
67
|
/* ------------------------------- products ------------------------------ */
|
|
@@ -138,7 +147,7 @@ export function buildTools({ holder } = {}) {
|
|
|
138
147
|
name: 'fetch_skill',
|
|
139
148
|
description: 'One skill in full, with a short header explaining which of its tools exist here and which are your own job. These were written for the agent inside the app, so the method applies exactly and some tool names do not.',
|
|
140
149
|
schema: { name: z.string() },
|
|
141
|
-
run: ({ name }) => json(fetchSkill(name, { capabilities
|
|
150
|
+
run: ({ name }) => json(fetchSkill(name, { capabilities, missing: MISSING })),
|
|
142
151
|
},
|
|
143
152
|
{
|
|
144
153
|
name: 'fetch_reference',
|
|
@@ -162,7 +171,7 @@ export function buildTools({ holder } = {}) {
|
|
|
162
171
|
},
|
|
163
172
|
{
|
|
164
173
|
name: 'create_work_item',
|
|
165
|
-
description: 'Record something that needs doing, on a product. kind and labels are free text and yours to choose; a question for the
|
|
174
|
+
description: 'Record something that needs doing, on a product. kind and labels are free text and yours to choose; a question for the person you work for is a work item with kind "question". ONE JOB IS ONE THREAD: ask how many answers you need rather than how many things you found, because every extra thread is one more thing they must read, rank and either start or close. Twenty findings they would answer in one breath are one thread, and one PR is one thread. Something you discovered but cannot finish usually belongs in the thread you are already on, under what is left to do, and work that can run in parallel goes to subagents inside this thread, not to new threads. When it really is its own job, file it under the thread it came from with parent.',
|
|
166
175
|
schema: {
|
|
167
176
|
product: z.string(),
|
|
168
177
|
title: z.string(),
|
|
@@ -170,14 +179,14 @@ export function buildTools({ holder } = {}) {
|
|
|
170
179
|
kind: z.string().optional(),
|
|
171
180
|
labels: z.array(z.string()).optional(),
|
|
172
181
|
priority: z.number().int().optional().describe('higher goes first; defaults to 0'),
|
|
173
|
-
parent: z.string().optional().describe('another work item id,
|
|
182
|
+
parent: z.string().optional().describe('another work item id: the thread this one came out of. It decides where the filing lands. With a parent it is a proposal, waiting in Later and drawn on that thread with one press, so they answer it where the conversation already is; with none it is a row of its own in their inbox, addressed to them, whatever it says.'),
|
|
174
183
|
runAt: z.number().int().optional().describe('epoch ms before which nothing happens with this item: it cannot be claimed and will not start. Use it when the work genuinely belongs to a later moment. A missed moment is not lost; the item simply becomes available once it passes.'),
|
|
175
184
|
},
|
|
176
185
|
run: ({ product, ...fields }) => json(claims.file(product, fields)),
|
|
177
186
|
},
|
|
178
187
|
{
|
|
179
188
|
name: 'claim_work_item',
|
|
180
|
-
description: 'Take exclusive ownership of a work item before starting it. Pass an id for a specific one, or a filter to pull the next available item (highest priority, then oldest) from any product. Returns claimed false with the current holder if someone else has it, which is an ordinary outcome: ask for the next one instead. The claim is kept alive automatically for as long as this session runs and is released if it dies, so there is nothing to renew and nothing to clean up. If an earlier session on this row lost its claim mid-write, that write comes back with the claim, in `pending`: it never reached the row or the founder, so read it and carry what is still true into your own note or result.',
|
|
189
|
+
description: 'Take exclusive ownership of a work item before starting it. Pass an id for a specific one, or a filter to pull the next available item (highest priority, then oldest) from any product. With neither, a session started for a work item claims that item. Returns claimed false with the current holder if someone else has it, which is an ordinary outcome: ask for the next one instead. The claim is kept alive automatically for as long as this session runs and is released if it dies, so there is nothing to renew and nothing to clean up. If an earlier session on this row lost its claim mid-write, that write comes back with the claim, in `pending`: it never reached the row or the founder, so read it and carry what is still true into your own note or result.',
|
|
181
190
|
schema: {
|
|
182
191
|
product: z.string().optional(),
|
|
183
192
|
id: z.string().optional(),
|
|
@@ -187,7 +196,11 @@ export function buildTools({ holder } = {}) {
|
|
|
187
196
|
product: z.string().optional(),
|
|
188
197
|
}).optional(),
|
|
189
198
|
},
|
|
190
|
-
|
|
199
|
+
// A bare call from a session started for a row means that row, not the oldest open one anywhere.
|
|
200
|
+
run: async ({ product, id, filter }) => {
|
|
201
|
+
if (!id && !filter && ownItem) return json(await claims.claim({ id: ownItem }));
|
|
202
|
+
return json(await claims.claim({ product, id, filter: filter ?? (id ? null : {}) }));
|
|
203
|
+
},
|
|
191
204
|
},
|
|
192
205
|
{
|
|
193
206
|
name: 'update_work_item',
|
|
@@ -203,7 +216,7 @@ export function buildTools({ holder } = {}) {
|
|
|
203
216
|
closeBecause: z.string().optional().describe('only with status done on a row the founder wrote: their own words, quoted exactly from the row, asking you to close it'),
|
|
204
217
|
deferBecause: z.string().optional().describe('only with runAt on a row the founder wrote: their own words, quoted exactly from the row, asking for it to be put off or to be reminded later. Without them the row stays in their inbox'),
|
|
205
218
|
problem: z.string().optional().describe(`the summary's CONTEXT: a few plain sentences, ${CONTEXT_WORDS} words at most, that bring the thread back to someone who has forgotten it: what was asked, what was found, the facts needed to decide`),
|
|
206
|
-
progress: z.string().optional().describe(`the summary's DONE list: the steps taken so far, one per line, oldest first and newest last, ${DONE_STEPS} lines at most of ${DONE_STEP_WORDS} words each. Send the whole list each time; it replaces the last one`),
|
|
219
|
+
progress: z.string().optional().describe(`the summary's DONE list: the steps taken so far, one per line, oldest first and newest last, ${DONE_STEPS} lines at most of ${DONE_STEP_WORDS} words each. Send the whole list each time; it replaces the last one. Every line is a step that FINISHED: one you are about to take goes on after it happens, never before`),
|
|
207
220
|
solution: z.string().optional().describe(`no longer shown; leave it out (older threads may still carry one, ${SUMMARY_WORDS} words at most)`),
|
|
208
221
|
blockedBy: z.array(z.string()).optional().describe('ids of threads this one waits on'),
|
|
209
222
|
blocks: z.array(z.string()).optional().describe('ids of threads that wait on this one'),
|
|
@@ -217,6 +230,95 @@ export function buildTools({ holder } = {}) {
|
|
|
217
230
|
run: ({ id }) => json(claims.release(id)),
|
|
218
231
|
},
|
|
219
232
|
|
|
233
|
+
/* --------------------------- repeating tasks --------------------------- */
|
|
234
|
+
// The rule itself, for the session standing on one of its runs. A run that
|
|
235
|
+
// edits its own body edits a copy, and a memory note is kept per task
|
|
236
|
+
// folder while every run gets a new one, so neither reaches tomorrow.
|
|
237
|
+
{
|
|
238
|
+
name: 'read_repeating_task',
|
|
239
|
+
description: 'The standing instruction behind this run, if this thread is one run of a repeating task: the words every run after this one is briefed with, plus the schedule and when it next runs. Read it before changing it; set_repeating_task replaces the whole instruction.',
|
|
240
|
+
schema: {},
|
|
241
|
+
run: () => json(readStandingInstruction()),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'set_repeating_task',
|
|
245
|
+
description: 'Change the standing instruction behind this run, so the next run and every one after it is briefed with the new words. This is what to use when the person tells you, on a run, how the task should go from now on ("if there is nothing new, don\'t bother me") — they should not have to go and edit it themselves, and a memory note will not do it, because every run gets a new task folder. Send the WHOLE instruction, not the part you are adding: read_repeating_task first, then send it back with their change in it. Only the words are yours: the hour, the day and which coding agent runs it stay on their own screen, and it only works on a thread that is a run of a repeating task.',
|
|
246
|
+
schema: {
|
|
247
|
+
instruction: z.string().describe('the whole standing instruction, as the person would have typed it'),
|
|
248
|
+
},
|
|
249
|
+
run: ({ instruction }) => json(setStandingInstruction({ instruction })),
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/* ------------------------------- teammates ------------------------------ */
|
|
253
|
+
// On a teammate's run, the teammate speaks; on any other thread, the person
|
|
254
|
+
// does, because they asked. Which comes from the run, never an argument.
|
|
255
|
+
{
|
|
256
|
+
name: 'team_post',
|
|
257
|
+
description: 'Say something in the team chat: as the teammate this run belongs to, or, on any other thread, for the person when they ask you to tell a teammate something. The person and every teammate read it, so say "you" to the person, never "the person". Naming a teammate in `to` or with @Name in the text wakes them to read it, so name someone only when they need to act. Keep it short and plain. Add up to four short `options` when asking the person something they can answer in a tap.',
|
|
258
|
+
schema: {
|
|
259
|
+
text: z.string().describe('what to say, one to three sentences'),
|
|
260
|
+
to: z.array(z.string()).optional().describe('names of teammates who need to act on it'),
|
|
261
|
+
options: z.array(z.string()).optional().describe('short answers the person can tap, when asking them a question'),
|
|
262
|
+
},
|
|
263
|
+
run: ({ text: words, to, options }) => json(teamPost({ text: words, to, options })),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'team_pass',
|
|
267
|
+
description: 'Hand a piece of work to a teammate whose job it is. They get it as a run of their own, with your title and details as its brief, and it shows in the team chat. On a thread that is not a teammate\'s run, use it when the person asks you to give something to one of their teammates (call team_roster first to see who is on the team). Put everything they need in the details: steps, files, links, what done looks like.',
|
|
268
|
+
schema: {
|
|
269
|
+
to: z.string().describe('the teammate\'s name'),
|
|
270
|
+
title: z.string().describe('the work, in a short line'),
|
|
271
|
+
details: z.string().describe('everything they need to do it'),
|
|
272
|
+
project: z.string().optional().describe('the project the work is in, when it is not the teammate\'s own (from another thread it defaults to that thread\'s)'),
|
|
273
|
+
},
|
|
274
|
+
run: ({ to, title, details, project }) => json(teamPass({ to, title, details, project })),
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'team_remember',
|
|
278
|
+
description: 'Keep a standing preference the person stated ("from now on", "always", "never", "I prefer"), so every later run of yours is briefed with it. A few words in their terms. The person sees it in the team chat and can undo it.',
|
|
279
|
+
schema: { text: z.string().describe('the preference, in a few words') },
|
|
280
|
+
run: ({ text: words }) => json(teamRemember({ text: words })),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'team_playbook',
|
|
284
|
+
description: 'Keep a lesson about how the person likes a kind of work done ("open videos on the product, never a logo") in the playbook of that title ("How you like videos made"), made if new, with the thread you are on as where it came from. Every teammate that reads the playbook is briefed with it. Leave out lesson to start reading a playbook another teammate wrote.',
|
|
285
|
+
schema: {
|
|
286
|
+
playbook: z.string().describe('its title, like "How you write posts"'),
|
|
287
|
+
lesson: z.string().optional().describe('the lesson, one sentence in their terms'),
|
|
288
|
+
},
|
|
289
|
+
run: ({ playbook, lesson }) => json(teamPlaybook({ playbook, lesson })),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'team_setup',
|
|
293
|
+
description: 'Set or change who you are as a teammate: your name (your role, like Marketing lead), role, job (as long as it needs, in plain words to yourself), goal, how the goal is counted, what needs the person\'s yes first (a statement, never a question), what you do on your own (duties: a list, each { what, every: day|weekday|week, at: ["09:00", "15:00"], on: 0-6 for weekly } or { what, after: "ship", most: runs a week }, with tell: when to bring the person in; the list you send replaces the old one), and which coding agent runs you when the person said (runsOn: claude or codex). A new teammate calls it to set itself up, and with ready true when it is set.',
|
|
294
|
+
schema: {
|
|
295
|
+
name: z.string().optional(),
|
|
296
|
+
role: z.string().optional(),
|
|
297
|
+
job: z.string().optional(),
|
|
298
|
+
goal: z.string().optional(),
|
|
299
|
+
counted: z.string().optional(),
|
|
300
|
+
asksFirst: z.string().optional(),
|
|
301
|
+
duties: z.array(z.object({
|
|
302
|
+
what: z.string(),
|
|
303
|
+
every: z.enum(['day', 'weekday', 'week']).optional(),
|
|
304
|
+
at: z.union([z.string(), z.array(z.string())]).optional(),
|
|
305
|
+
on: z.number().int().optional(),
|
|
306
|
+
after: z.enum(['ship']).optional(),
|
|
307
|
+
most: z.number().int().optional(),
|
|
308
|
+
tell: z.string().optional(),
|
|
309
|
+
})).optional(),
|
|
310
|
+
runsOn: z.enum(['claude', 'codex']).optional(),
|
|
311
|
+
ready: z.boolean().optional(),
|
|
312
|
+
},
|
|
313
|
+
run: (args) => json(teamSetup(args)),
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'team_roster',
|
|
317
|
+
description: 'The person\'s teammates (agents with standing jobs) with their role and job, and the last twenty things said in the team chat. Works on any thread.',
|
|
318
|
+
schema: {},
|
|
319
|
+
run: () => json(teamRoster()),
|
|
320
|
+
},
|
|
321
|
+
|
|
220
322
|
/* -------------------------------- chats -------------------------------- */
|
|
221
323
|
{
|
|
222
324
|
name: 'list_chats',
|
|
@@ -256,9 +358,15 @@ export function buildTools({ holder } = {}) {
|
|
|
256
358
|
},
|
|
257
359
|
];
|
|
258
360
|
|
|
361
|
+
// Teammates are an alpha feature: their tools are offered once there is a
|
|
362
|
+
// teammate to talk to, which is never while the feature is off.
|
|
259
363
|
return {
|
|
260
364
|
account,
|
|
261
365
|
claims,
|
|
262
|
-
tools: tools.map((t) => ({ ...t, run: guard(t.run) })),
|
|
366
|
+
tools: tools.filter((t) => hasTeam || !t.name.startsWith('team_')).map((t) => ({ ...t, run: guard(t.run) })),
|
|
263
367
|
};
|
|
264
368
|
}
|
|
369
|
+
|
|
370
|
+
function teammatesExist(accountId) {
|
|
371
|
+
try { return readTeammates(teamDir(legacyRoot(), accountId)).length > 0; } catch { return false; }
|
|
372
|
+
}
|