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,580 @@
|
|
|
1
|
+
// Teammates in the main process: the files, the tick that carries out
|
|
2
|
+
// `planWakes`, and the brief a teammate's run is given. The decisions are in
|
|
3
|
+
// shared/teammates.mjs; this only reads, writes and starts.
|
|
4
|
+
import {
|
|
5
|
+
planWakes, teammateLabel, FROM_LABEL_PREFIX, HOP_LABEL_PREFIX, hopOf, teammateIdOf, YOU, mentionsIn,
|
|
6
|
+
letterFor, statusOf, runLines, STARTERS, CHAT_TAIL, isWorks, DEFAULT_LIMIT, MAX_HOPS,
|
|
7
|
+
offerFrom, offerAnswer, threadsDigest, OFFER_LABEL, OFFER_MAKE, OFFER_NOT_NOW, TEAMMATE_EFFORT, GREETING, starterAsks,
|
|
8
|
+
dutyWhen, dutyOf, DUTY_LABEL_PREFIX, CHAT_LABEL, yesFirst,
|
|
9
|
+
} from '../shared/teammates.mjs';
|
|
10
|
+
import { isShippedNote } from '../shared/shipped-note.mjs';
|
|
11
|
+
import { ruleLabel, periodKey } from '../shared/repeats.mjs';
|
|
12
|
+
import {
|
|
13
|
+
teamDir, readTeammates, readChat, writeTeammate, writeChat, newTeammateId, newMemoryId, readPlaybooks,
|
|
14
|
+
} from './store/teammates-files.mjs';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const CHAT_FOR_SCREEN = 400;
|
|
19
|
+
|
|
20
|
+
const TROUBLE_VERB = { run: 'start a run', answer: 'get your message', release: 'start its next run', seen: 'mark a message read', served: 'mark its round served', dutyServed: 'mark a duty done' };
|
|
21
|
+
|
|
22
|
+
// When each project last shipped, from the note the ship queue leaves.
|
|
23
|
+
function shippedByProduct(items) {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const i of items ?? []) {
|
|
26
|
+
if (!isShippedNote(i.note)) continue;
|
|
27
|
+
const at = i.wrote?.note?.ts ?? i.updatedAt ?? 0;
|
|
28
|
+
if (at > (out[i.product] ?? 0)) out[i.product] = at;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const capital = (s) => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
|
34
|
+
|
|
35
|
+
function lastRanOf(teammateId, dutyId, items) {
|
|
36
|
+
let at = null;
|
|
37
|
+
for (const i of items) if (teammateIdOf(i) === teammateId && dutyOf(i) === dutyId && (i.createdAt ?? 0) > (at ?? 0)) at = i.createdAt;
|
|
38
|
+
return at;
|
|
39
|
+
}
|
|
40
|
+
const TROUBLE_SHOWN_MS = 30 * 60_000;
|
|
41
|
+
|
|
42
|
+
export class Teammates {
|
|
43
|
+
constructor(store) {
|
|
44
|
+
this.store = store;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dir() {
|
|
48
|
+
return teamDir(this.store.config.storeRoot, this.store.config.accountId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
list(now = Date.now()) {
|
|
52
|
+
return readTeammates(this.dir(), now);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get(id, now = Date.now()) {
|
|
56
|
+
return this.list(now).find((t) => t.id === id) ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
chat() {
|
|
60
|
+
return readChat(this.dir());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A project a teammate can work in: the one asked for when it exists, else
|
|
64
|
+
// the first with code in it, since most of what a teammate does is code.
|
|
65
|
+
_product(slug) {
|
|
66
|
+
const products = this.store.listProducts().filter((p) => !p.practice && !p.personal);
|
|
67
|
+
return products.find((p) => p.slug === slug)?.slug ?? products.find((p) => p.repoPath)?.slug ?? products[0]?.slug ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A project named on a pass, if it is one a teammate can work in.
|
|
71
|
+
_productIf(slug) {
|
|
72
|
+
if (!slug) return null;
|
|
73
|
+
return this.store.listProducts().some((p) => p.slug === slug && !p.practice && !p.personal) ? slug : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
create(fields = {}, now = Date.now()) {
|
|
77
|
+
const starter = fields.starter ? STARTERS.find((s) => s.starter === fields.starter) : null;
|
|
78
|
+
const base = { ...(starter ?? {}), ...fields };
|
|
79
|
+
const product = this._product(base.product);
|
|
80
|
+
if (!product) throw new Error('Make a project first. A teammate works in one of your projects.');
|
|
81
|
+
const team = this.list(now);
|
|
82
|
+
const name = String(base.name ?? '').trim() || 'New teammate';
|
|
83
|
+
const id = newTeammateId();
|
|
84
|
+
const patch = {
|
|
85
|
+
name,
|
|
86
|
+
letter: base.letter || letterFor(name, team),
|
|
87
|
+
role: base.role ?? null,
|
|
88
|
+
job: base.job ?? null,
|
|
89
|
+
goal: base.goal ?? null,
|
|
90
|
+
counted: base.counted ?? null,
|
|
91
|
+
asksFirst: base.asksFirst ?? null,
|
|
92
|
+
works: isWorks(base.works) ? base.works : null,
|
|
93
|
+
product,
|
|
94
|
+
engine: base.engine ?? null,
|
|
95
|
+
model: base.model ?? null,
|
|
96
|
+
effort: base.effort ?? null,
|
|
97
|
+
limit: Number.isInteger(base.limit) ? base.limit : DEFAULT_LIMIT,
|
|
98
|
+
status: base.status === 'draft' ? 'draft' : 'active',
|
|
99
|
+
starter: starter?.starter ?? null,
|
|
100
|
+
createdAt: now,
|
|
101
|
+
paused: false,
|
|
102
|
+
};
|
|
103
|
+
// Its first round is the next one, not one already past today.
|
|
104
|
+
if (patch.works) patch.served = periodKey(patch.works, now) ?? '';
|
|
105
|
+
writeTeammate(this.dir(), id, patch, now);
|
|
106
|
+
return this.get(id, now);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Someone new, from words: a draft that sets itself up by asking you in the
|
|
110
|
+
// team chat. Its first run is the setup.
|
|
111
|
+
// `greeted` is the New teammate page: the greeting it drew goes in the chat
|
|
112
|
+
// first, from the teammate, so your answer reads as one.
|
|
113
|
+
describe({ text, starter, product, engine, threads, greeted = false } = {}, now = Date.now()) {
|
|
114
|
+
const s = starter ? STARTERS.find((x) => x.starter === starter) : null;
|
|
115
|
+
if (starter && !s) throw new Error('No such starter.');
|
|
116
|
+
const words = String(s?.ask ?? text ?? '').trim();
|
|
117
|
+
if (!words) throw new Error('Say what you need them to do.');
|
|
118
|
+
const team = this.list(now);
|
|
119
|
+
const offered = starterAsks(team).map((a) => a.ask);
|
|
120
|
+
const t = s
|
|
121
|
+
? this.create({ starter: s.starter, status: 'draft', product, engine, ...(team.some((x) => x.name === s.name) ? { name: 'New teammate' } : {}) }, now)
|
|
122
|
+
: this.create({ name: 'New teammate', status: 'draft', job: words, product, engine }, now);
|
|
123
|
+
if (greeted) writeChat(this.dir(), { kind: 'msg', from: t.id, to: [], text: GREETING, options: offered, ts: now - 1 }, now);
|
|
124
|
+
const m = writeChat(this.dir(), { kind: 'msg', from: YOU, to: [t.id], text: words, setup: true, ...(threads?.length ? { threads } : {}) }, now);
|
|
125
|
+
return { teammate: this.get(t.id, now), message: m.id };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
start(id, now = Date.now()) {
|
|
129
|
+
const t = this.get(id, now);
|
|
130
|
+
if (!t) throw new Error('No such teammate.');
|
|
131
|
+
if (t.status !== 'active') writeTeammate(this.dir(), id, { status: 'active' }, now);
|
|
132
|
+
writeChat(this.dir(), { kind: 'act', from: id, verb: 'started' }, now);
|
|
133
|
+
writeChat(this.dir(), { kind: 'msg', from: YOU, to: [id], text: 'You have just been started. Introduce yourself in one or two sentences, say what you will do first, then begin.', start: true }, now);
|
|
134
|
+
return this.get(id, now);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
update(id, patch = {}, now = Date.now()) {
|
|
138
|
+
const t = this.get(id, now);
|
|
139
|
+
if (!t) throw new Error('No such teammate.');
|
|
140
|
+
const allowed = ['name', 'role', 'job', 'goal', 'counted', 'asksFirst', 'works', 'product', 'engine', 'model', 'effort', 'limit', 'paused', 'letter'];
|
|
141
|
+
const clean = {};
|
|
142
|
+
for (const k of allowed) if (k in patch) clean[k] = patch[k];
|
|
143
|
+
if ('product' in clean) clean.product = this._product(clean.product);
|
|
144
|
+
if (clean.works && isWorks(clean.works)) clean.served = periodKey(clean.works, now) ?? '';
|
|
145
|
+
if ('paused' in clean && !!clean.paused !== !!t.paused) {
|
|
146
|
+
writeChat(this.dir(), { kind: 'act', from: id, verb: clean.paused ? 'paused' : 'resumed' }, now);
|
|
147
|
+
}
|
|
148
|
+
writeTeammate(this.dir(), id, clean, now);
|
|
149
|
+
// Its open runs follow it from their next turn. A model is one engine's
|
|
150
|
+
// name, so a move to another engine clears it.
|
|
151
|
+
const effort = ('effort' in clean ? clean.effort : t.effort) || TEAMMATE_EFFORT;
|
|
152
|
+
if ('engine' in clean && (clean.engine ?? null) !== (t.engine ?? null)) this._moveOpenRuns(id, { engine: clean.engine ?? '', model: '', effort }, now);
|
|
153
|
+
else if (('model' in clean && (clean.model ?? null) !== (t.model ?? null)) || ('effort' in clean && (clean.effort ?? null) !== (t.effort ?? null))) {
|
|
154
|
+
this._moveOpenRuns(id, { model: ('model' in clean ? clean.model : t.model) ?? '', effort }, now);
|
|
155
|
+
}
|
|
156
|
+
return this.get(id, now);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_moveOpenRuns(id, patch, now) {
|
|
160
|
+
const { workItemsDisk } = this.store.modules;
|
|
161
|
+
let items = [];
|
|
162
|
+
try { items = this.store.listItems(now); } catch { return; }
|
|
163
|
+
for (const run of items.filter((i) => teammateIdOf(i) === id && i.status !== 'done')) {
|
|
164
|
+
const dir = this.store.productDir?.(run.product);
|
|
165
|
+
if (!dir) continue;
|
|
166
|
+
try {
|
|
167
|
+
workItemsDisk.updateWorkItem(dir, run.id, patch, { source: 'system', now });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
this.noteTrouble(`A run of this teammate stayed on its old agent: ${err.message}`, now);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
remove(id, now = Date.now()) {
|
|
175
|
+
writeTeammate(this.dir(), id, { endedAt: now }, now);
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
remember(id, text, { from = null, by = null } = {}, now = Date.now()) {
|
|
180
|
+
const words = String(text ?? '').trim();
|
|
181
|
+
if (!words) throw new Error('Nothing to remember.');
|
|
182
|
+
const mem = { id: newMemoryId(), text: words.slice(0, 300), from, at: now, by };
|
|
183
|
+
writeTeammate(this.dir(), id, { remember: mem }, now);
|
|
184
|
+
writeChat(this.dir(), { kind: 'act', from: id, verb: 'learned', text: mem.text, ref: mem.id }, now);
|
|
185
|
+
return mem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
forget(id, memoryId, now = Date.now()) {
|
|
189
|
+
writeTeammate(this.dir(), id, { forget: memoryId }, now);
|
|
190
|
+
return { ok: true };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Your words in the team chat. On a teammate's page they go to that
|
|
194
|
+
// teammate; in the team chat to whoever they name with @.
|
|
195
|
+
post({ text, to = [], options, fresh = false } = {}, now = Date.now()) {
|
|
196
|
+
const words = String(text ?? '').trim();
|
|
197
|
+
if (!words) throw new Error('Nothing to send.');
|
|
198
|
+
const team = this.list(now);
|
|
199
|
+
const ids = new Set([...(Array.isArray(to) ? to : [to]).filter(Boolean), ...mentionsIn(words, team)]);
|
|
200
|
+
const known = [...ids].filter((x) => team.some((t) => t.id === x));
|
|
201
|
+
return writeChat(this.dir(), { kind: 'msg', from: YOU, to: known, text: words.slice(0, 8000), ...(options ? { options } : {}), ...(fresh ? { fresh: true } : {}) }, now);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Your go-ahead for work held after too many hand-offs: the same work again,
|
|
205
|
+
// from you, so it wakes the teammate with everything it carried.
|
|
206
|
+
goAhead(id, ref, now = Date.now()) {
|
|
207
|
+
const held = this.chat().messages.find((m) => m.id === ref);
|
|
208
|
+
if (!held) throw new Error('That work is no longer in the chat.');
|
|
209
|
+
if (held.kind === 'pass') {
|
|
210
|
+
return writeChat(this.dir(), { kind: 'pass', from: YOU, to: id, title: held.title, details: held.details ?? '', ...(held.product ? { product: held.product } : {}) }, now);
|
|
211
|
+
}
|
|
212
|
+
const from = this.nameOf(held.from, this.list(now));
|
|
213
|
+
return writeChat(this.dir(), { kind: 'msg', from: YOU, to: [id], text: `Go ahead with what ${from} wrote:\n\n> ${String(held.text ?? '').split('\n').join('\n> ')}` }, now);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// What the screen draws: every teammate with what it is doing, and the chat
|
|
217
|
+
// with the lines its runs leave, newest last.
|
|
218
|
+
view(items, now = Date.now()) {
|
|
219
|
+
let team = [];
|
|
220
|
+
let chat = { messages: [] };
|
|
221
|
+
try {
|
|
222
|
+
team = this.list(now);
|
|
223
|
+
chat = this.chat();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { list: [], chat: [], runs: [], error: err.message };
|
|
226
|
+
}
|
|
227
|
+
const mine = (items ?? []).filter((i) => teammateIdOf(i));
|
|
228
|
+
let books = [];
|
|
229
|
+
try { books = readPlaybooks(this.dir()); } catch { /* the playbooks file is read again next time */ }
|
|
230
|
+
return {
|
|
231
|
+
...(this.trouble && now - this.trouble.at < TROUBLE_SHOWN_MS ? { trouble: this.trouble.text } : {}),
|
|
232
|
+
list: team.map((t) => ({
|
|
233
|
+
...t, ...statusOf(t, mine, now),
|
|
234
|
+
// A name that already is the role is not followed by it again.
|
|
235
|
+
role: t.role && t.role.toLowerCase() === String(t.name).toLowerCase() ? null : t.role,
|
|
236
|
+
worksLabel: isWorks(t.works) ? ruleLabel(t.works) : null,
|
|
237
|
+
duties: (t.duties ?? []).map((d) => ({ id: d.id, what: d.what, when: dutyWhen(d), lastRan: lastRanOf(t.id, d.id, mine) })),
|
|
238
|
+
playbooks: books.filter((b) => b.readers.includes(t.id)).map((b) => b.id),
|
|
239
|
+
})),
|
|
240
|
+
chat: chat.messages.slice(-CHAT_FOR_SCREEN),
|
|
241
|
+
runs: runLines(mine, team).slice(-CHAT_FOR_SCREEN),
|
|
242
|
+
playbooks: books,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ----------------------------------------------------------------- tick */
|
|
247
|
+
// Carries out what `planWakes` decided. Each action is written so a crash
|
|
248
|
+
// between two of them redoes nothing twice: runs are created if absent under
|
|
249
|
+
// the project lock, and the seen marker comes after the run it records.
|
|
250
|
+
async tick({ items, now = Date.now(), deliver } = {}) {
|
|
251
|
+
let team;
|
|
252
|
+
let chat;
|
|
253
|
+
try {
|
|
254
|
+
team = this.list(now);
|
|
255
|
+
chat = this.chat();
|
|
256
|
+
} catch (err) {
|
|
257
|
+
this.noteTrouble(`Teammates could not be read: ${err.message}`, now);
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await this.offer({ items, chat, now });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
this.noteTrouble(`The offer of a teammate failed: ${err.message}`, now);
|
|
264
|
+
}
|
|
265
|
+
if (!team.length) return [];
|
|
266
|
+
team = this._namedByRole(team, now);
|
|
267
|
+
const mine = (items ?? []).filter((i) => teammateIdOf(i));
|
|
268
|
+
const actions = planWakes({ teammates: team, chat, items: mine, now, shipped: shippedByProduct(items) });
|
|
269
|
+
const byId = new Map(team.map((t) => [t.id, t]));
|
|
270
|
+
const done = [];
|
|
271
|
+
const { workItemsDisk, lock } = this.store.modules;
|
|
272
|
+
// What did not go through is not marked seen or served, so the next tick
|
|
273
|
+
// tries it again.
|
|
274
|
+
const failed = new Set();
|
|
275
|
+
for (const a of actions) {
|
|
276
|
+
const t = byId.get(a.teammate);
|
|
277
|
+
if (a.type === 'seen' && failed.has(a.msg)) continue;
|
|
278
|
+
if (a.type === 'served' && failed.has(`${a.teammate}@${a.key}`)) continue;
|
|
279
|
+
if (a.type === 'dutyServed' && failed.has(`${a.teammate}#${a.duty}@${a.key}`)) continue;
|
|
280
|
+
try {
|
|
281
|
+
if (a.type === 'run') {
|
|
282
|
+
const product = this._productIf(a.trigger.product) ?? this._product(t.product);
|
|
283
|
+
if (!product) continue;
|
|
284
|
+
const dir = this.store.productDir(product);
|
|
285
|
+
const hop = a.trigger.hop ?? 0;
|
|
286
|
+
const labels = ['founder', teammateLabel(t.id), `${HOP_LABEL_PREFIX}${hop}`];
|
|
287
|
+
if (a.trigger.kind === 'pass' && a.trigger.from && a.trigger.from !== YOU) labels.push(`${FROM_LABEL_PREFIX}${a.trigger.from}`);
|
|
288
|
+
if (a.trigger.kind === 'duty') labels.push(`${DUTY_LABEL_PREFIX}${a.trigger.duty}`);
|
|
289
|
+
if (a.trigger.kind === 'message' && a.trigger.from === YOU && !a.trigger.via) labels.push(CHAT_LABEL);
|
|
290
|
+
await lock.withProjectLock(dir, async () => {
|
|
291
|
+
const opening = this.openingFor(t, a.trigger, team);
|
|
292
|
+
const made = workItemsDisk.createWorkItemIfAbsent(dir, a.id, {
|
|
293
|
+
title: a.title,
|
|
294
|
+
...(opening.yours ? {} : { body: opening.body }),
|
|
295
|
+
kind: 'directive',
|
|
296
|
+
priority: 5,
|
|
297
|
+
labels,
|
|
298
|
+
...(a.start === 'later' ? { start: 'later' } : {}),
|
|
299
|
+
...(t.engine ? { engine: t.engine } : {}),
|
|
300
|
+
...(t.model ? { model: t.model } : {}),
|
|
301
|
+
effort: t.effort || TEAMMATE_EFFORT,
|
|
302
|
+
}, { source: 'system', now });
|
|
303
|
+
// Your words go in as yours, the way a task you type does, so the
|
|
304
|
+
// thread opens on them under your name.
|
|
305
|
+
if (made?.created !== false && opening.yours) workItemsDisk.updateWorkItem(dir, a.id, { title: a.title, body: opening.body }, { source: 'founder', now });
|
|
306
|
+
});
|
|
307
|
+
} else if (a.type === 'seen') {
|
|
308
|
+
writeChat(this.dir(), { kind: 'seen', ref: a.msg, for: a.teammate, ...(a.run ? { run: a.run } : {}) }, now);
|
|
309
|
+
} else if (a.type === 'served') {
|
|
310
|
+
writeTeammate(this.dir(), a.teammate, { served: a.key }, now);
|
|
311
|
+
} else if (a.type === 'dutyServed') {
|
|
312
|
+
writeTeammate(this.dir(), a.teammate, { dutyServed: { [a.duty]: a.key } }, now);
|
|
313
|
+
} else if (a.type === 'answer') {
|
|
314
|
+
await deliver?.(a.product, a.item, a.text);
|
|
315
|
+
} else if (a.type === 'release') {
|
|
316
|
+
const dir = this.store.productDir(a.product);
|
|
317
|
+
workItemsDisk.updateWorkItem(dir, a.item, { start: 'now' }, { source: 'system', now });
|
|
318
|
+
} else if (a.type === 'limit') {
|
|
319
|
+
this._sayOnce(a.teammate, 'limit', `${t.name} has used ${t.limit ?? DEFAULT_LIMIT} runs this week and waits until runs free up. Your own messages still reach it.`, now);
|
|
320
|
+
} else if (a.type === 'hops') {
|
|
321
|
+
writeChat(this.dir(), { kind: 'act', from: a.teammate, verb: 'hops', ref: a.msg, text: `${t.name} is holding this until you say go. Teammates had passed it back and forth ${MAX_HOPS} times without you.` }, now);
|
|
322
|
+
}
|
|
323
|
+
done.push(a);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (a.msg) failed.add(a.msg);
|
|
326
|
+
if (a.trigger?.msg) failed.add(a.trigger.msg);
|
|
327
|
+
if (a.trigger?.key) failed.add(a.trigger.kind === 'duty' ? `${a.teammate}#${a.trigger.duty}@${a.trigger.key}` : `${a.teammate}@${a.trigger.key}`);
|
|
328
|
+
this.noteTrouble(`${t?.name ?? 'A teammate'} could not ${TROUBLE_VERB[a.type] ?? 'carry on'}: ${err.message}`, now);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (done.length && done.length === actions.length) this.trouble = null;
|
|
332
|
+
return done;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The offer of a teammate for asks you keep making: acts on a tap on an
|
|
336
|
+
// open one, then makes a new one if your threads call for it.
|
|
337
|
+
async offer({ items = [], chat = this.chat(), now = Date.now() } = {}) {
|
|
338
|
+
const { workItemsDisk, lock } = this.store.modules;
|
|
339
|
+
for (const row of items.filter((i) => (i.labels ?? []).includes(OFFER_LABEL) && i.status !== 'done')) {
|
|
340
|
+
const tap = offerAnswer(row.answer);
|
|
341
|
+
if (!tap) continue;
|
|
342
|
+
const dir = this.store.productDir(row.product);
|
|
343
|
+
if (tap.kind === 'make') {
|
|
344
|
+
const listed = String(row.result ?? '').split('\n').filter((l) => l.startsWith('- '));
|
|
345
|
+
const threads = this._threadsBehind(row, listed, items, chat);
|
|
346
|
+
const asks = listed.join('\n');
|
|
347
|
+
const read = threads.length ? `\n\nRead all ${threads.length} threads behind them first, with everything I said in them.` : '';
|
|
348
|
+
const text = tap.words
|
|
349
|
+
? `${tap.words}\n\nLike these asks of mine:\n${asks}${read}`
|
|
350
|
+
: `Someone to take on asks like these, which I keep making:\n${asks}${read}`;
|
|
351
|
+
this.describe({ text, product: row.product, threads }, now);
|
|
352
|
+
}
|
|
353
|
+
// Clean, so the answered offer leaves Needs you rather than waiting to be
|
|
354
|
+
// read, and titled for what happened, so it no longer reads as an offer.
|
|
355
|
+
const like = String(row.title ?? '').match(/“(.+)”/)?.[1];
|
|
356
|
+
workItemsDisk.updateWorkItem(dir, row.id, {
|
|
357
|
+
...(like ? { title: tap.kind === 'make' ? `Made a teammate for things like “${like}”` : `No teammate for things like “${like}”` } : {}),
|
|
358
|
+
status: 'done',
|
|
359
|
+
labels: [...(row.labels ?? []), 'clean'],
|
|
360
|
+
note: tap.kind === 'make' ? 'A new teammate is setting itself up in the team chat.' : 'Not now. This one will not be offered again.',
|
|
361
|
+
}, { source: 'system', now });
|
|
362
|
+
}
|
|
363
|
+
const offered = new Set(chat.messages.filter((m) => m.kind === 'act' && m.verb === 'offered').map((m) => m.ref));
|
|
364
|
+
const offer = offerFrom({ items, offered, now });
|
|
365
|
+
if (!offer) return null;
|
|
366
|
+
const dir = this.store.productDir(offer.product);
|
|
367
|
+
if (!dir) return null;
|
|
368
|
+
const body = offer.said;
|
|
369
|
+
const result = [
|
|
370
|
+
'**Make a teammate for these?**', '', `${offer.said} Like these:`, '', ...offer.asked.map(({ title, n }) => `- ${title}${n > 1 ? ` (${n} times)` : ''}`), '',
|
|
371
|
+
'A teammate would take this kind of work on. It does it on its own, on a schedule if you like, and comes to you only when it needs you. It reads these threads first, with everything you said in them, then asks you only what they leave open.',
|
|
372
|
+
'', '## Options', `1. ${OFFER_MAKE} (recommended)`, `2. ${OFFER_NOT_NOW}`,
|
|
373
|
+
].join('\n');
|
|
374
|
+
// Recorded first, so a crash after it can never offer the same job twice.
|
|
375
|
+
writeChat(this.dir(), { kind: 'act', from: YOU, verb: 'offered', ref: offer.key, text: offer.title, threads: offer.threads }, now);
|
|
376
|
+
await lock.withProjectLock(dir, async () => {
|
|
377
|
+
const made = workItemsDisk.createWorkItem(dir, {
|
|
378
|
+
title: offer.title, body, kind: 'question', priority: 5, labels: [OFFER_LABEL],
|
|
379
|
+
problem: 'A teammate could do this kind of work on its own.', progress: `Found ${offer.count} asks like these this month`,
|
|
380
|
+
}, { source: 'system', now });
|
|
381
|
+
workItemsDisk.updateWorkItem(dir, made.id, { result }, { source: 'agent', now });
|
|
382
|
+
});
|
|
383
|
+
return offer;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// The threads an answered offer came from. An offer made before it kept
|
|
387
|
+
// them is matched back to your asks by the titles it listed.
|
|
388
|
+
_threadsBehind(row, listed, items, chat) {
|
|
389
|
+
const kept = chat.messages.find((m) => m.kind === 'act' && m.verb === 'offered' && m.text === row.title && m.threads?.length);
|
|
390
|
+
if (kept) return kept.threads;
|
|
391
|
+
const titles = new Set(listed.map((l) => l.slice(2).replace(/ \(\d+ times\)$/, '').trim().toLowerCase()));
|
|
392
|
+
return items.filter((i) => (i.labels ?? []).includes('founder') && !teammateIdOf(i) && titles.has(String(i.label || i.title).trim().toLowerCase()))
|
|
393
|
+
.slice(0, 12).map((i) => ({ product: i.product, id: i.id, title: String(i.label || i.title).trim() }));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Said on the teammates page and in the log, never only in the log.
|
|
397
|
+
noteTrouble(text, now = Date.now()) {
|
|
398
|
+
console.warn(`teammates: ${text}`);
|
|
399
|
+
this.trouble = { text, at: now };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
_sayOnce(teammateId, verb, text, now) {
|
|
403
|
+
const day = new Date(now).toDateString();
|
|
404
|
+
const already = this.chat().messages.some((m) => m.kind === 'act' && m.from === teammateId && m.verb === verb && new Date(m.ts).toDateString() === day);
|
|
405
|
+
if (!already) writeChat(this.dir(), { kind: 'act', from: teammateId, verb, text }, now);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Teammates go by their role. Ones made with a person's name are renamed
|
|
409
|
+
// once; a name you give one afterwards is yours and stays.
|
|
410
|
+
_namedByRole(team, now) {
|
|
411
|
+
const marker = path.join(this.dir(), 'named-by-role');
|
|
412
|
+
if (fs.existsSync(marker)) return team;
|
|
413
|
+
const renamed = new Map();
|
|
414
|
+
const taken = new Set(team.filter((t) => !t.role || t.name.toLowerCase() === t.role.toLowerCase()).map((t) => t.name.toLowerCase()));
|
|
415
|
+
for (const t of team) {
|
|
416
|
+
if (!t.role || t.name.toLowerCase() === t.role.toLowerCase()) continue;
|
|
417
|
+
const base = capital(t.role.trim()).slice(0, 36);
|
|
418
|
+
let name = base;
|
|
419
|
+
for (let n = 2; taken.has(name.toLowerCase()); n++) name = `${base} ${n}`;
|
|
420
|
+
taken.add(name.toLowerCase());
|
|
421
|
+
renamed.set(t.id, name);
|
|
422
|
+
}
|
|
423
|
+
const out = [];
|
|
424
|
+
for (const t of team) {
|
|
425
|
+
const name = renamed.get(t.id);
|
|
426
|
+
if (!name) { out.push(t); continue; }
|
|
427
|
+
const letter = letterFor(name, out.concat(team.filter((x) => !renamed.has(x.id) && x.id !== t.id)));
|
|
428
|
+
writeTeammate(this.dir(), t.id, { name, letter }, now);
|
|
429
|
+
out.push({ ...t, name, letter });
|
|
430
|
+
}
|
|
431
|
+
try { fs.mkdirSync(this.dir(), { recursive: true }); fs.writeFileSync(marker, `${now}\n`); } catch { /* tried again next tick */ }
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* ---------------------------------------------------------------- brief */
|
|
436
|
+
nameOf(id, team) {
|
|
437
|
+
if (id === YOU) return 'The person';
|
|
438
|
+
return team.find((t) => t.id === id)?.name ?? 'A teammate';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Why this run exists, written into the run's own body.
|
|
442
|
+
// What opens the run's thread: your own words when you wrote, and otherwise
|
|
443
|
+
// a plain line saying what woke it. How to answer is in its brief.
|
|
444
|
+
openingFor(t, trigger, team) {
|
|
445
|
+
const who = this.nameOf(trigger.from, team);
|
|
446
|
+
if (trigger.kind === 'schedule') {
|
|
447
|
+
return { body: `Time for your ${isWorks(t.works) ? ruleLabel(t.works) : 'regular'} round.`, yours: false };
|
|
448
|
+
}
|
|
449
|
+
if (trigger.kind === 'duty') {
|
|
450
|
+
const tell = trigger.tell ? `Bring the person in only when ${trigger.tell.replace(/[.\s]+$/, '')}.` : 'Bring the person in only with something worth their time.';
|
|
451
|
+
return { body: [
|
|
452
|
+
`One of your duties: ${trigger.what.replace(/[.\s]+$/, '')}.`,
|
|
453
|
+
'',
|
|
454
|
+
'Check first, cheaply, whether there is anything worth doing now. If nothing is worth it, do nothing else: post nothing, and close this run with one line saying what you checked.',
|
|
455
|
+
tell,
|
|
456
|
+
].join('\n'), yours: false };
|
|
457
|
+
}
|
|
458
|
+
if (trigger.kind === 'pass') {
|
|
459
|
+
const by = trigger.via ? 'One of your threads' : who;
|
|
460
|
+
return { body: [`${by} passed you this in the team chat.`, '', `## ${trigger.title ?? 'Work passed on'}`, '', trigger.details ?? ''].join('\n').trim(), yours: false };
|
|
461
|
+
}
|
|
462
|
+
const text = String(trigger.text ?? '').trim() || '(no words)';
|
|
463
|
+
// Pressing Start is not writing these words, so they are not shown as yours.
|
|
464
|
+
if (trigger.opening === 'start') return { body: `Started from the Teammates page. ${text.replace(/^You have just been started\.\s*/, '')}`, yours: false };
|
|
465
|
+
// Nor is what a thread wrote for you: it says which thread spoke.
|
|
466
|
+
if (trigger.via) return { body: `One of your threads wrote this to ${t.name} for you:\n\n> ${text.split('\n').join('\n> ')}`, yours: false };
|
|
467
|
+
if (trigger.from === YOU) return { body: text, yours: true };
|
|
468
|
+
return { body: `${who} wrote to you in the team chat:\n\n> ${text.split('\n').join('\n> ')}`, yours: false };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Where the person's work lives. A teammate works in one project, but a job
|
|
472
|
+
// like fundraising draws on all of them: the power test's investor update
|
|
473
|
+
// read one repository and missed what had shipped in the others.
|
|
474
|
+
_projectsFor(t) {
|
|
475
|
+
let products = [];
|
|
476
|
+
try { products = this.store.listProducts().filter((p) => !p.practice); } catch { return []; }
|
|
477
|
+
if (products.length < 2) return [];
|
|
478
|
+
return [
|
|
479
|
+
"## The person's projects",
|
|
480
|
+
...products.map((p) => `- ${p.name}${p.slug === t.product ? ' (yours)' : ''}: ${p.repoPath ? `code at ${p.repoPath}` : 'no code'}${p.dir ? `, docs at ${p.dir}` : ''}`),
|
|
481
|
+
'Read any of them when your job needs it. Change files only in your own, unless the person asks.',
|
|
482
|
+
'',
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// The standing part of a teammate's brief, read fresh at every spawn so its
|
|
487
|
+
// memory and the chat are current even on a reply.
|
|
488
|
+
briefFor(item, now = Date.now()) {
|
|
489
|
+
const id = teammateIdOf(item);
|
|
490
|
+
if (!id) return null;
|
|
491
|
+
let team;
|
|
492
|
+
let chat;
|
|
493
|
+
try {
|
|
494
|
+
team = this.list(now);
|
|
495
|
+
chat = this.chat();
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const t = team.find((x) => x.id === id);
|
|
500
|
+
if (!t) return null;
|
|
501
|
+
const items = (() => { try { return this.store.listItems(now).filter((i) => teammateIdOf(i)); } catch { return []; } })();
|
|
502
|
+
const others = team.filter((x) => x.id !== id);
|
|
503
|
+
const tail = chat.messages.filter((m) => m.kind === 'msg' || m.kind === 'pass' || (m.kind === 'act' && m.verb === 'learned')).slice(-CHAT_TAIL);
|
|
504
|
+
const line = (m) => {
|
|
505
|
+
const when = new Date(m.ts).toLocaleString('en-US', { weekday: 'short', hour: 'numeric', minute: '2-digit' });
|
|
506
|
+
const from = this.nameOf(m.from, team);
|
|
507
|
+
if (m.kind === 'pass') return `[${when}] ${from} passed to ${this.nameOf(m.to, team)}: ${m.title}`;
|
|
508
|
+
if (m.kind === 'act') return `[${when}] ${from} learned: ${m.text}`;
|
|
509
|
+
const to = (m.to ?? []).length ? ` (to ${(m.to).map((x) => this.nameOf(x, team)).join(', ')})` : '';
|
|
510
|
+
return `[${when}] ${from}${to}: ${String(m.text ?? '').replace(/\s+/g, ' ').slice(0, 600)}`;
|
|
511
|
+
};
|
|
512
|
+
const hop = hopOf(item);
|
|
513
|
+
const from = t.status === 'draft' ? chat.messages.find((m) => m.kind === 'msg' && m.setup && (m.to ?? []).includes(id) && m.threads?.length)?.threads : null;
|
|
514
|
+
// From the history kept per ledger change, not readThread: that reads the
|
|
515
|
+
// whole ledger once per thread, up to twelve times in one brief.
|
|
516
|
+
const said = (product, thread) => this.store.modules.workItemsCore.threadOf(this.store.readHistory?.(product, thread, { max: 2000 }) ?? [], thread);
|
|
517
|
+
const digest = from ? threadsDigest(from, said) : '';
|
|
518
|
+
let books = [];
|
|
519
|
+
try { books = readPlaybooks(this.dir()); } catch { books = []; }
|
|
520
|
+
const reads = books.filter((b) => b.readers.includes(t.id));
|
|
521
|
+
const notRead = books.filter((b) => !b.readers.includes(t.id));
|
|
522
|
+
const cut = (text, n) => (text.length > n ? `${text.slice(0, n - 1)}…` : text);
|
|
523
|
+
return [
|
|
524
|
+
`# You are ${t.name}${t.role && t.role.toLowerCase() !== t.name.toLowerCase() ? `, the ${t.role}` : ''}: a teammate`,
|
|
525
|
+
'',
|
|
526
|
+
`You are one of the person's teammates in {{Name}}: an agent with a standing job that works on its own, talks with the other teammates in one team chat, and reaches the person only when it needs them. This thread is one run of your work, and its opening message is why you are awake.`,
|
|
527
|
+
'',
|
|
528
|
+
t.job ? `Your job: ${t.job}` : null,
|
|
529
|
+
t.goal ? `Your goal: ${t.goal}${t.counted ? ` (counted as: ${t.counted})` : ''}` : null,
|
|
530
|
+
yesFirst(t.asksFirst) ? `Ask the person first: ${yesFirst(t.asksFirst)}`
|
|
531
|
+
: t.asksFirst ? "What needs the person's yes first is not set: what you saved there is a question. Ask it with team_post, with options, and save their answer as a statement with team_setup asksFirst." : null,
|
|
532
|
+
t.duties?.length ? null : isWorks(t.works) ? `You wake on your own ${ruleLabel(t.works)}.` : 'You wake when someone writes to you or passes you work.',
|
|
533
|
+
'',
|
|
534
|
+
...(t.duties?.length ? ['## What you do on your own', ...t.duties.map((d) => `- ${d.what} (${dutyWhen(d).toLowerCase()})${d.tell ? `. Bring the person in only when ${d.tell}` : ''}`), 'You also wake when someone writes to you or passes you work.', ''] : []),
|
|
535
|
+
t.memory.length ? '## What you know about the person (follow these)' : null,
|
|
536
|
+
...t.memory.map((m) => `- ${m.text}`),
|
|
537
|
+
t.memory.length ? '' : null,
|
|
538
|
+
...(reads.length ? ['## Playbooks you read (how the person likes this work done; follow these)', ...reads.flatMap((b) => [`### ${b.title}`, ...b.lessons.map((l) => `- ${cut(l.text, 300)}`)]), ''].slice(0, 120) : []),
|
|
539
|
+
...(notRead.length ? [`Playbooks on the team you do not read yet: ${notRead.map((b) => b.title).join(', ')}. Start reading one with team_playbook and its title, leaving out lesson, when its work is yours too.`, ''] : []),
|
|
540
|
+
...this._projectsFor(t),
|
|
541
|
+
'## The team',
|
|
542
|
+
...(others.length ? others.map((o) => { const s = statusOf(o, items, now); return `- ${o.name}${o.role && o.role.toLowerCase() !== o.name.toLowerCase() ? `, ${o.role}` : ''}${o.paused ? ' (paused)' : ''}: ${o.job ? o.job.slice(0, 200) : 'no job yet'}${s.now ? ` Now: ${s.now}.` : ''}`; }) : ['- You are the only teammate so far.']),
|
|
543
|
+
'',
|
|
544
|
+
'## The team chat, newest last',
|
|
545
|
+
...(tail.length ? tail.map(line) : ['(nothing yet)']),
|
|
546
|
+
'',
|
|
547
|
+
...(digest ? ['## The threads you come from', 'You were made from these threads, where the person asked for this kind of work: what they asked, what the agent did, and every reply they gave.', '', digest, ''] : []),
|
|
548
|
+
'## Answering what woke you',
|
|
549
|
+
...(t.status === 'draft' ? [
|
|
550
|
+
'You are a new teammate, still being set up. The opening message is what the person asked for, and anything they have answered since is in the team chat above. Set yourself up, then stop:',
|
|
551
|
+
digest ? '0. First read every thread under "The threads you come from" above for how the person wants this work done: what they asked for, what they corrected, what they liked and turned down, and what they decided. Keep each lasting lesson about how this kind of work is done with team_playbook (a short title like "How you write posts"), and anything about the person themselves with team_remember, one per call, in their words. Write your job, goal and what needs their yes from what the threads show, not from guesses, and in step 2 ask only what the threads leave open.' : null,
|
|
552
|
+
'Your job, goal, when you wake and what you ask first, above, are guesses to confirm, not settled.',
|
|
553
|
+
'1. Your name is your role: two or three words that say what you do, like Marketing lead or Support lead, that no teammate has. Set name and role to it. If you already have a name other than New teammate, keep that name. Draft your job in plain words to yourself, as long as it needs, your goal, how you count it, what you ask the person first, your duties (what you do on your own and when; each time you wake on your own is a duty, and your job promises no time your duties do not hold), and if they said which agent should run you, runsOn. Save the draft with team_setup, without ready.',
|
|
554
|
+
'2. Always ask before you start, even when the opening seems clear: the person does not want a teammate that guesses. Ask what you would otherwise assume, at least when you should work on your own and what you should do first, and anything else you guessed, such as what needs their yes. One team_post per question, at most four, each with two to four short answers in `options`, your best guess first. Then finish this run quietly. Their answers wake you again with the chat in your brief.',
|
|
555
|
+
'3. When they have answered, save what they said with team_setup and ready true, post one or two sentences saying what you will do first, and finish quietly. team_setup refuses ready until they have written to you.',
|
|
556
|
+
'If the opening is a piece of work rather than a description of a teammate, it is also your first job: set yourself up for work like it, and once team_setup has ready true, do it in this run and finish as any run does.',
|
|
557
|
+
] : [
|
|
558
|
+
'- If the person wrote to you, the opening is their words from the team chat. Reply to them there with team_post, and do what they ask if it is part of your job.',
|
|
559
|
+
'- If a teammate passed you work, do it if it is in your job, then say in the team chat what you did. If it is not yours to do, say so and whose it is.',
|
|
560
|
+
'- If a teammate wrote to you, reply in the team chat. Start with @Name only if they need to act on it.',
|
|
561
|
+
'- If it is your round, do your job for today, as above.',
|
|
562
|
+
]),
|
|
563
|
+
'',
|
|
564
|
+
'## How a teammate works',
|
|
565
|
+
'- The person reads the team chat, your answers, and your job and goal. Say "you" to them in all of it ("You never miss a price change"), never "the person".',
|
|
566
|
+
'- Talk with team_post: short and plain, one to three sentences. Post when you start something worth knowing, when you finish, and when you hand something on. Naming someone with @Name wakes them, so do that only when they need to act.',
|
|
567
|
+
'- Hand work to a teammate whose job it is with team_pass (to, title, details). They get it as a run of their own. Do not do another teammate\'s job. In the details, point at the threads, files and links it needs rather than retelling them, so nothing is lost on the way.',
|
|
568
|
+
'- When the person states a standing preference ("from now on", "always", "never", "I prefer"), keep it with team_remember in a few words, in their terms.',
|
|
569
|
+
'- When they show how they like a kind of work done (a correction, a choice between two, "too slow at the start"), keep the lesson with team_playbook under that kind of work, so every teammate doing it learns it too. One lesson per call.',
|
|
570
|
+
'- When they change your job, what needs their yes, or what you do on your own, save it with team_setup. The chat shows them it saved; do not ask them to confirm the wording.',
|
|
571
|
+
'- Reach the person only when you need them: a decision, a yes before anything that leaves (sends, posts, ships, merges, pays), or a sign-in only they have. Then end this run with your question as your answer: bold first line, numbered options, your recommendation first. It reaches their inbox. Leave this item open.',
|
|
572
|
+
`- When nothing needs them, post a one-line summary to the team chat, then close this run yourself: update_work_item with status done and labels ["founder", "${teammateLabel(t.id)}", "${HOP_LABEL_PREFIX}${hop}", "clean"] in one call. This run is your own work, not the person's ask, so closing it needs no quote from them.`,
|
|
573
|
+
'- Never wait or poll for a teammate. Say what you are waiting for in the chat and finish; their reply wakes you.',
|
|
574
|
+
'- When the person changes your job, goal or when you work, save it with team_setup (every, at, on) and say so. Your schedule is yours to set that way; never send them to a menu for it.',
|
|
575
|
+
'- Hand work on with team_pass, not create_work_item: a thread you file waits in their Later list for them to start it.',
|
|
576
|
+
].filter((x) => x !== null).join('\n');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export { STARTERS };
|