volute 0.52.0 → 0.54.0
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/dist/{accept-5NK5XJB5.js → accept-RSDIVJL6.js} +3 -0
- package/dist/auto-upgrade-HEVZKPTF.js +46 -0
- package/dist/{backup-YVGV2ZNM.js → backup-QRNX6RK7.js} +10 -10
- package/dist/channels-CKCY64TD.js +203 -0
- package/dist/{chat-VAZ25KVT.js → chat-ZUGNL5GS.js} +5 -5
- package/dist/chunk-3LHUXY3E.js +102 -0
- package/dist/{chunk-SW2U6HRB.js → chunk-444RR3YF.js} +21 -4
- package/dist/{chunk-YVR6EHWL.js → chunk-56IT2ZXF.js} +160 -18
- package/dist/chunk-5RBEEAKW.js +58 -0
- package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
- package/dist/{chunk-G7A5LEVI.js → chunk-7XKPG66D.js} +5 -5
- package/dist/{chunk-R772ET2M.js → chunk-AAPNU7GA.js} +4 -0
- package/dist/{chunk-XRWUQPCA.js → chunk-ATZF2RNJ.js} +25 -3
- package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
- package/dist/{chunk-JHKBALS2.js → chunk-CLCHOXLR.js} +1 -1
- package/dist/{chunk-DMK5ASKV.js → chunk-F6D766VQ.js} +6 -2
- package/dist/{chunk-AXFLY5GU.js → chunk-HKGYZVWN.js} +16 -1
- package/dist/{chunk-ZEZQNP2G.js → chunk-LGXVGIWN.js} +103 -25
- package/dist/{chunk-CW7BQKF6.js → chunk-LKUGUQUW.js} +3 -3
- package/dist/{chunk-FXWBFDQR.js → chunk-OAYY3WF3.js} +59 -12
- package/dist/{chunk-ITK6HV47.js → chunk-OGPVJ53N.js} +14 -6
- package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
- package/dist/chunk-OT5SXF7L.js +904 -0
- package/dist/{chunk-EDR47UE2.js → chunk-OYF25B62.js} +1 -1
- package/dist/{chunk-RPPTGPYF.js → chunk-PPSN2MLC.js} +2 -2
- package/dist/{chunk-K2F237KS.js → chunk-RGHF3OK5.js} +154 -18
- package/dist/{chunk-RU74YG46.js → chunk-VDUCLVZI.js} +338 -30
- package/dist/{chunk-724DVYHU.js → chunk-WHL6PKYP.js} +10 -10
- package/dist/{chunk-EEOMA4DX.js → chunk-WZZCA23D.js} +2 -1
- package/dist/{chunk-UDZLPR34.js → chunk-YSRYCS3N.js} +13 -8
- package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
- package/dist/{chunk-VPOMJMV6.js → chunk-ZNHL5KQJ.js} +1 -1
- package/dist/cli.js +17 -17
- package/dist/{clock-ZT5YT6HA.js → clock-X7V3C7NL.js} +2 -2
- package/dist/{cloud-sync-H7EWZASD.js → cloud-sync-52YNJRMR.js} +12 -11
- package/dist/{daemon-restart-4FWUA5JF.js → daemon-restart-QWHN5VMU.js} +4 -4
- package/dist/daemon.js +538 -782
- package/dist/{db-IPXRJUI7.js → db-I4EH5SLF.js} +3 -1
- package/dist/{delivery-manager-VYXVKUMP.js → delivery-manager-7R2YIQ2W.js} +13 -7
- package/dist/delivery-notices-RZQ6GONP.js +18 -0
- package/dist/{delivery-router-RHANNTAC.js → delivery-router-QS4NLJR6.js} +3 -1
- package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
- package/dist/{echo-text-PEEV7FQU.js → echo-text-73CYQRL7.js} +14 -13
- package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
- package/dist/{extensions-N6L5NYTK.js → extensions-QT65JGV6.js} +22 -11
- package/dist/{files-BRY5RRIM.js → files-PCKBQHAY.js} +2 -1
- package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
- package/dist/{message-delivery-7URC52ME.js → message-delivery-TRKT4T2P.js} +16 -13
- package/dist/migrate-name-placeholder-C7NCNLWF.js +37 -0
- package/dist/{mind-NEXGT3KE.js → mind-CT6ANXK4.js} +11 -11
- package/dist/{mind-contacts-QYU4TPEA.js → mind-contacts-PEXCDRXA.js} +6 -6
- package/dist/{mind-history-OGNR4L7W.js → mind-history-RZMMCHMX.js} +83 -11
- package/dist/{mind-list-SYRA7CH5.js → mind-list-7KBLPLES.js} +1 -1
- package/dist/{mind-service-E26ZQXDS.js → mind-service-7EFIJ3ZA.js} +12 -11
- package/dist/{mind-sleep-LZAFBDZD.js → mind-sleep-24HV4RCA.js} +2 -2
- package/dist/{mind-status-Z2LQFA6F.js → mind-status-4YW4PHMO.js} +15 -0
- package/dist/{package-2W2XKSIP.js → package-F6S5NHY2.js} +4 -4
- package/dist/{prompts-NBO3HRK3.js → prompts-JECJIFPQ.js} +1 -1
- package/dist/{reject-TWB4QJHG.js → reject-4NFLXKRN.js} +6 -2
- package/dist/{scheduler-JTTWZDYU.js → scheduler-73Q4MQ7Y.js} +8 -6
- package/dist/{seed-64XEHRUP.js → seed-DRKAP3XV.js} +1 -1
- package/dist/{seed-cmd-UEL2QB3Q.js → seed-cmd-6IMTO3LG.js} +2 -2
- package/dist/{seed-create-4JRQDPGA.js → seed-create-VNLDYRFK.js} +1 -1
- package/dist/{seed-readiness-DYOVCYHN.js → seed-readiness-H5TTPAQH.js} +2 -2
- package/dist/{seed-sprout-NIDIOJXT.js → seed-sprout-PPGYGK4G.js} +5 -5
- package/dist/{send-2YKOZJJ5.js → send-UXUQKO4N.js} +37 -22
- package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
- package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
- package/dist/{setup-4IX6PCD3.js → setup-YUAQMDVZ.js} +4 -4
- package/dist/skills/dreaming/SKILL.md +2 -2
- package/dist/skills/dreaming/references/INSTALL.md +1 -1
- package/dist/skills/memory/SKILL.md +47 -43
- package/dist/skills/tending/SKILL.md +2 -0
- package/dist/skills/volute-mind/references/integrations.md +3 -2
- package/dist/skills/volute-mind/references/routing.md +18 -5
- package/dist/{skills-MPUALV3S.js → skills-GMQ5UIIW.js} +2 -2
- package/dist/{sleep-manager-OFP2GFXF.js → sleep-manager-KHI65FEX.js} +12 -11
- package/dist/{spirit-4ECYIVGE.js → spirit-Q5VGFWUI.js} +19 -3
- package/dist/{spirit-availability-Y54Y3675.js → spirit-availability-G5FMWKRH.js} +13 -12
- package/dist/{sprout-GJMZFRGZ.js → sprout-PMTRL7QN.js} +1 -1
- package/dist/{src-LCSOQY6Z.js → src-3FIW7DLH.js} +241 -87
- package/dist/src-H3UCOP6B.js +516 -0
- package/dist/{src-HGNE2XG4.js → src-LTTQVN2Z.js} +1 -1
- package/dist/{status-MWLE3RVC.js → status-LCKTTIWS.js} +2 -2
- package/dist/{system-events-V43HDA3T.js → system-events-Q7IVZ5T4.js} +5 -1
- package/dist/{systems-VDRUZXNF.js → systems-HL7SGSW7.js} +2 -2
- package/dist/{turn-tracker-C5PVIC3B.js → turn-tracker-HZDPMSCC.js} +4 -2
- package/dist/{up-H6LJZDRK.js → up-XZDSJVMU.js} +3 -3
- package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
- package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
- package/dist/{variant-cleanup-STC6B5HR.js → variant-cleanup-QHGGY7ND.js} +8 -6
- package/dist/{version-notify-LBVHH5YV.js → version-notify-VNVFX6UP.js} +22 -7
- package/dist/web-assets/assets/{index-B_eYTa3R.js → index-CwtxGwe_.js} +16 -16
- package/dist/web-assets/assets/index-DhM1yYRN.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +4 -4
- package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
- package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +67 -0
- package/packages/extensions/{plan → intentions}/dist/ui/index.html +3 -3
- package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
- package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
- package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
- package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
- package/packages/extensions/pages/skills/pages/SKILL.md +18 -13
- package/templates/_base/.init/.config/prompts.json +3 -3
- package/templates/{pi → _base}/.init/.config/routes.json +1 -0
- package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/codex-session-seed.ts +463 -0
- package/templates/_base/src/lib/context-breakdown.ts +11 -3
- package/templates/_base/src/lib/daemon-client.ts +35 -0
- package/templates/_base/src/lib/pi-session-seed.ts +434 -0
- package/templates/_base/src/lib/routing.ts +2 -2
- package/templates/_base/src/lib/seed-note.ts +78 -0
- package/templates/_base/src/lib/session-seed.ts +375 -0
- package/templates/_base/src/lib/startup.ts +112 -6
- package/templates/claude/.init/CLAUDE.md +7 -3
- package/templates/claude/src/agent.ts +295 -101
- package/templates/claude/src/lib/hooks/pre-compact.ts +30 -5
- package/templates/claude/src/lib/message-channel.ts +43 -15
- package/templates/claude/src/lib/recover.ts +69 -0
- package/templates/claude/src/lib/stream-consumer.ts +33 -2
- package/templates/claude/src/server.ts +2 -1
- package/templates/claude/volute-template.json +1 -1
- package/templates/codex/.init/AGENTS.md +7 -3
- package/templates/codex/src/agent.ts +256 -39
- package/templates/codex/src/server.ts +2 -1
- package/templates/codex/volute-template.json +1 -1
- package/templates/pi/.init/MINDS.md +7 -3
- package/templates/pi/src/agent.ts +269 -43
- package/templates/pi/src/lib/event-handler.ts +11 -0
- package/templates/pi/src/lib/mechanics-doc.ts +30 -0
- package/templates/pi/src/server.ts +5 -1
- package/templates/pi/volute-template.json +1 -1
- package/dist/channels-FSAHOQWI.js +0 -90
- package/dist/skills/plan-coordinator/SKILL.md +0 -60
- package/dist/src-UDMJZ6MH.js +0 -426
- package/dist/web-assets/assets/index-DhZrzddW.css +0 -1
- package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
- package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
- package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
- package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
- package/templates/claude/.init/.config/routes.json +0 -11
- package/templates/codex/.init/.config/routes.json +0 -11
- package/dist/{env-IBW2QI6K.js → env-7AZFBPWX.js} +5 -5
- package/dist/{login-RJJTCVR3.js → login-4NUS72Z5.js} +3 -3
- package/dist/{login-SB5OENMV.js → login-57MMYILL.js} +4 -4
- package/dist/{register-OX4HRSGC.js → register-WNCSOTAU.js} +3 -3
- package/dist/{restart-AGEGNSYQ.js → restart-B4GNMVF2.js} +4 -4
- package/dist/{skill-B5OJEQUA.js → skill-QYTBUDNQ.js} +4 -4
- package/dist/{start-MUMNFR52.js → start-BDHJVOVX.js} +3 -3
- package/dist/{stop-EKERNYTX.js → stop-45QYSGYS.js} +4 -4
|
@@ -24,11 +24,25 @@ import {
|
|
|
24
24
|
import { daemonEmit } from "./lib/daemon-client.js";
|
|
25
25
|
import { dispatchPrompt } from "./lib/dispatch.js";
|
|
26
26
|
import { createEventHandler, emit } from "./lib/event-handler.js";
|
|
27
|
+
import { compactTimestamp } from "./lib/format-prefix.js";
|
|
27
28
|
import { runHooks } from "./lib/hook-loader.js";
|
|
28
29
|
import { log } from "./lib/logger.js";
|
|
30
|
+
import {
|
|
31
|
+
computePiSeedCut,
|
|
32
|
+
DEFAULT_SEED_TOKENS,
|
|
33
|
+
type PiSeedCut,
|
|
34
|
+
rotatePiSession,
|
|
35
|
+
seedPiSession,
|
|
36
|
+
} from "./lib/pi-session-seed.js";
|
|
29
37
|
import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
|
|
30
38
|
import { resolveModel } from "./lib/resolve-model.js";
|
|
31
|
-
import {
|
|
39
|
+
import { buildSeededNote, type SeedCause } from "./lib/seed-note.js";
|
|
40
|
+
import {
|
|
41
|
+
getStartupContext,
|
|
42
|
+
loadPrompts,
|
|
43
|
+
renderCompactionWarning,
|
|
44
|
+
type SubagentConfig,
|
|
45
|
+
} from "./lib/startup.js";
|
|
32
46
|
import { createSubagentExtension, type SubagentDefinition } from "./lib/subagents.js";
|
|
33
47
|
import type {
|
|
34
48
|
HandlerMeta,
|
|
@@ -52,16 +66,51 @@ type PiSession = {
|
|
|
52
66
|
currentMessageId?: string;
|
|
53
67
|
messageChannels: Map<string, { channel: string; sender?: string }>;
|
|
54
68
|
contextTokens: number;
|
|
69
|
+
/**
|
|
70
|
+
* True when this session was seeded from the previous session's transcript.
|
|
71
|
+
* Consumed once, on the first turn, to inject the honest-boundary note.
|
|
72
|
+
*/
|
|
73
|
+
seeded?: boolean;
|
|
74
|
+
/** When the seeded-from session was archived (epoch ms), for the gap note; null if unknown. */
|
|
75
|
+
seededArchivedAt?: number | null;
|
|
76
|
+
/** Why the tail is seeded — picks the boundary note's wording. Last cause wins. */
|
|
77
|
+
seededCause?: SeedCause;
|
|
78
|
+
/**
|
|
79
|
+
* Set after an in-place rotation so the mind is told, on its next turn, that the
|
|
80
|
+
* session rotated at the context limit. Delivered separately from the restored-seed
|
|
81
|
+
* note (which fires only on the first turn of a freshly created agent session).
|
|
82
|
+
*/
|
|
83
|
+
rotationNotePending?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The cut point pinned at warn time (the first-kept turn the mind was told would
|
|
86
|
+
* survive verbatim). Honored at rotation so the promise in the warning holds even
|
|
87
|
+
* though the transcript grows after the warning. Null until a warning fires.
|
|
88
|
+
*/
|
|
89
|
+
rotationCut?: PiSeedCut | null;
|
|
90
|
+
/**
|
|
91
|
+
* Back-to-back rotations that did NOT bring context under the threshold (reset by
|
|
92
|
+
* any healthy turn). Guards against a runaway loop when the tail alone can't fit —
|
|
93
|
+
* e.g. a system prompt (large MEMORY.md) that already fills most of the window,
|
|
94
|
+
* which rotation can't trim. Past the cap we defer to the SDK's native compaction.
|
|
95
|
+
*/
|
|
96
|
+
consecutiveRotations?: number;
|
|
55
97
|
};
|
|
56
98
|
|
|
99
|
+
/** Stop self-rotating after this many back-to-back rotations that didn't reduce context. */
|
|
100
|
+
const MAX_CONSECUTIVE_ROTATIONS = 3;
|
|
101
|
+
|
|
57
102
|
export function createMind(options: {
|
|
58
103
|
systemPrompt: string;
|
|
59
104
|
cwd: string;
|
|
60
105
|
mindDir: string;
|
|
106
|
+
/** Directory holding pi session subdirs (`<sessionsDir>/<name>/*.jsonl`). */
|
|
107
|
+
sessionsDir: string;
|
|
61
108
|
model?: string;
|
|
62
109
|
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
63
110
|
compactionMessage?: string;
|
|
64
111
|
maxContextTokens?: number;
|
|
112
|
+
/** Estimated-token budget for seeding a fresh persistent session. 0 disables. Default 30000. */
|
|
113
|
+
seedTokens?: number;
|
|
65
114
|
subagents?: Record<string, SubagentConfig>;
|
|
66
115
|
}): {
|
|
67
116
|
resolve: HandlerResolver;
|
|
@@ -70,12 +119,11 @@ export function createMind(options: {
|
|
|
70
119
|
} {
|
|
71
120
|
const sessions = new Map<string, PiSession>();
|
|
72
121
|
const prompts = loadPrompts();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} in prompt template
|
|
76
|
-
options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
|
|
122
|
+
const compactionMessage = () =>
|
|
123
|
+
options.compactionMessage ?? renderCompactionWarning(prompts.compaction_warning);
|
|
77
124
|
const compactionInstructions = prompts.compaction_instructions;
|
|
78
125
|
const maxContextTokens = options.maxContextTokens;
|
|
126
|
+
const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
|
|
79
127
|
|
|
80
128
|
if (maxContextTokens) {
|
|
81
129
|
log("mind", `compaction threshold: ${maxContextTokens} tokens`);
|
|
@@ -183,6 +231,22 @@ export function createMind(options: {
|
|
|
183
231
|
// Inject startup context on the first turn of each session
|
|
184
232
|
if (!startupContextInjected) {
|
|
185
233
|
startupContextInjected = true;
|
|
234
|
+
// On the first turn of a seeded session, lead with the honest-boundary
|
|
235
|
+
// note (consumed once) so the restored transcript above isn't mistaken
|
|
236
|
+
// for a continuously-lived conversation.
|
|
237
|
+
if (session.seeded) {
|
|
238
|
+
session.seeded = false;
|
|
239
|
+
const note = buildSeededNote({
|
|
240
|
+
cause: "restored",
|
|
241
|
+
archivedAtMs: session.seededArchivedAt ?? null,
|
|
242
|
+
});
|
|
243
|
+
emit(session, {
|
|
244
|
+
type: "context",
|
|
245
|
+
content: note,
|
|
246
|
+
metadata: { source: "seeded-session" },
|
|
247
|
+
});
|
|
248
|
+
parts.push(note);
|
|
249
|
+
}
|
|
186
250
|
const startupContext = await startupContextPromise;
|
|
187
251
|
if (startupContext) {
|
|
188
252
|
emit(session, {
|
|
@@ -194,6 +258,20 @@ export function createMind(options: {
|
|
|
194
258
|
}
|
|
195
259
|
}
|
|
196
260
|
|
|
261
|
+
// On the first turn after an in-place rotation, tell the mind the earlier
|
|
262
|
+
// turns collapsed at the context limit (distinct from the restored-seed note,
|
|
263
|
+
// which only fires on a freshly created agent session). Consumed once.
|
|
264
|
+
if (session.rotationNotePending) {
|
|
265
|
+
session.rotationNotePending = false;
|
|
266
|
+
const note = buildSeededNote({ cause: "rotation" });
|
|
267
|
+
emit(session, {
|
|
268
|
+
type: "context",
|
|
269
|
+
content: note,
|
|
270
|
+
metadata: { source: "seeded-session" },
|
|
271
|
+
});
|
|
272
|
+
parts.push(note);
|
|
273
|
+
}
|
|
274
|
+
|
|
197
275
|
try {
|
|
198
276
|
const result = await runHooks(hooksDir, "pre-prompt", {
|
|
199
277
|
event: "pre-prompt",
|
|
@@ -273,16 +351,49 @@ export function createMind(options: {
|
|
|
273
351
|
async function initSession(session: PiSession) {
|
|
274
352
|
const isEphemeral = session.name.startsWith("new-");
|
|
275
353
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
354
|
+
// Fresh persistent session — seed it from the previous session's archived
|
|
355
|
+
// transcript so the mind experiences the conversation continuing rather than
|
|
356
|
+
// waking into an empty context. seedPiSession no-ops when a live session
|
|
357
|
+
// already exists (continueRecent will resume that instead). Ephemeral
|
|
358
|
+
// `new-*` sessions are one-offs — they never seed at start.
|
|
359
|
+
const seeded = isEphemeral
|
|
360
|
+
? null
|
|
361
|
+
: seedPiSession({
|
|
362
|
+
cwd: options.cwd,
|
|
363
|
+
piSessionsDir: options.sessionsDir,
|
|
364
|
+
name: session.name,
|
|
365
|
+
seedTokens,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Every session (ephemeral too) is file-backed under its own name-scoped dir,
|
|
369
|
+
// so rotation applies uniformly at the context limit. Ephemerality is only that
|
|
370
|
+
// `new-*` sessions never seed at start and never archive on rotation — not a
|
|
371
|
+
// different backing store. Their unique `new-<ts>-<rand>` names mean the dir is
|
|
372
|
+
// always fresh, so continueRecent starts them empty (no seed to adopt).
|
|
373
|
+
const sessionManager = SessionManager.continueRecent(
|
|
374
|
+
options.cwd,
|
|
375
|
+
resolvePath(options.sessionsDir, session.name),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// If continueRecent adopted our seed file, its header id is the seeded id;
|
|
379
|
+
// if it fell back to a truly fresh session it minted a different id, so the
|
|
380
|
+
// honest-boundary note never lies about a conversation that didn't continue.
|
|
381
|
+
if (seeded && sessionManager.getSessionId() === seeded.sessionId) {
|
|
382
|
+
session.seeded = true;
|
|
383
|
+
session.seededArchivedAt = seeded.archivedAt;
|
|
384
|
+
session.seededCause = "restored";
|
|
385
|
+
log("mind", `session "${session.name}": seeded from previous transcript`);
|
|
386
|
+
}
|
|
279
387
|
|
|
280
388
|
log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
|
|
281
389
|
|
|
282
|
-
// Compaction state machine:
|
|
283
|
-
// 1. onContextTokens
|
|
390
|
+
// Compaction state machine (rotation, not SDK /compact):
|
|
391
|
+
// 1. threshold (onContextTokens) or native PreCompact -> warnAndPinRotation:
|
|
392
|
+
// pin the cut from the live transcript, interpolate it into the ${cutoff}
|
|
393
|
+
// warning, send it as the wrap-up prompt, set compactionTriggered
|
|
284
394
|
// 2. onTurnEnd (after warning turn): compactionTriggered -> compactOnNextTurnEnd
|
|
285
|
-
// 3. onTurnEnd (after mind's
|
|
395
|
+
// 3. onTurnEnd (after mind's wrap-up turn): compactOnNextTurnEnd -> rotateInPlace
|
|
396
|
+
// (or, past the runaway cap, the native compact() backstop)
|
|
286
397
|
let compactBlocked = false;
|
|
287
398
|
let manualCompactPending = false;
|
|
288
399
|
let compactionTriggered = false;
|
|
@@ -295,31 +406,144 @@ export function createMind(options: {
|
|
|
295
406
|
compactionInProgress = false;
|
|
296
407
|
}
|
|
297
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Warn the mind that the session will rotate at the context limit and pin the cut
|
|
411
|
+
* point it's told about. Computes which whole turns fit in `seedTokens` from the
|
|
412
|
+
* live transcript, pins the first-kept turn (so rotation honors the same boundary
|
|
413
|
+
* even as the transcript grows), interpolates its local time into the `${cutoff}`
|
|
414
|
+
* placeholder, and sends the warning as the wrap-up prompt. Shared by the
|
|
415
|
+
* threshold and native-PreCompact paths.
|
|
416
|
+
*/
|
|
417
|
+
function warnAndPinRotation() {
|
|
418
|
+
let cutoffLabel = "the cutoff";
|
|
419
|
+
session.rotationCut = null;
|
|
420
|
+
const sourcePath = session.agentSession?.sessionManager.getSessionFile();
|
|
421
|
+
if (sourcePath) {
|
|
422
|
+
try {
|
|
423
|
+
const cut = computePiSeedCut(readFileSync(sourcePath, "utf-8"), seedTokens);
|
|
424
|
+
if (cut) {
|
|
425
|
+
session.rotationCut = cut;
|
|
426
|
+
if (cut.boundaryTimestamp) {
|
|
427
|
+
const d = new Date(cut.boundaryTimestamp);
|
|
428
|
+
if (!Number.isNaN(d.getTime())) cutoffLabel = compactTimestamp(d);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
log("mind", `session "${session.name}": failed to pin rotation cut:`, err);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff} prompt placeholder
|
|
436
|
+
const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
|
|
437
|
+
compactionTriggered = true;
|
|
438
|
+
session.messageIds.push(undefined);
|
|
439
|
+
session.agentSession?.prompt(warning, { streamingBehavior: "followUp" });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Rotate the session in place onto a synthetic session holding the verbatim recent
|
|
444
|
+
* tail (from the pinned cut), then switch the running SessionManager to it. Returns
|
|
445
|
+
* false if rotation can't proceed (session not ready, or the file build failed) so
|
|
446
|
+
* the caller can fall back to a fresh session. The setSessionFile + state.messages
|
|
447
|
+
* re-sync is the load-bearing adoption step: the agent caches context in
|
|
448
|
+
* state.messages, so swapping the file alone wouldn't change what the mind sees
|
|
449
|
+
* (this mirrors the SDK's own compact() re-sync).
|
|
450
|
+
*/
|
|
451
|
+
function rotateInPlace(): boolean {
|
|
452
|
+
const as = session.agentSession;
|
|
453
|
+
const sourcePath = as?.sessionManager.getSessionFile();
|
|
454
|
+
if (!as || !sourcePath) return false; // not ready — fall back to fresh
|
|
455
|
+
const newPath = rotatePiSession({
|
|
456
|
+
cwd: options.cwd,
|
|
457
|
+
sessionsDir: options.sessionsDir,
|
|
458
|
+
name: session.name,
|
|
459
|
+
sourcePath,
|
|
460
|
+
cut: session.rotationCut ?? null,
|
|
461
|
+
seedTokens,
|
|
462
|
+
});
|
|
463
|
+
session.rotationCut = null;
|
|
464
|
+
if (!newPath) return false;
|
|
465
|
+
as.sessionManager.setSessionFile(newPath);
|
|
466
|
+
as.state.messages = as.sessionManager.buildSessionContext().messages;
|
|
467
|
+
session.rotationNotePending = true;
|
|
468
|
+
session.seededCause = "rotation";
|
|
469
|
+
session.consecutiveRotations = (session.consecutiveRotations ?? 0) + 1;
|
|
470
|
+
compactBlocked = false; // re-arm the native-compaction backstop
|
|
471
|
+
log(
|
|
472
|
+
"mind",
|
|
473
|
+
`session "${session.name}": rotated in place → ${as.sessionManager.getSessionId()} (rotation ${session.consecutiveRotations})`,
|
|
474
|
+
);
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Rotation couldn't proceed — start a fresh session in place (new empty transcript,
|
|
480
|
+
* cleared context), matching the claude template's rotation-failure fallback. Fresh
|
|
481
|
+
* is a far smaller cliff now that seeding exists; crucially it is NOT a silent native
|
|
482
|
+
* compaction.
|
|
483
|
+
*/
|
|
484
|
+
function freshFallback() {
|
|
485
|
+
const as = session.agentSession;
|
|
486
|
+
if (as) {
|
|
487
|
+
as.sessionManager.newSession();
|
|
488
|
+
as.state.messages = [];
|
|
489
|
+
}
|
|
490
|
+
session.rotationCut = null;
|
|
491
|
+
session.consecutiveRotations = 0;
|
|
492
|
+
compactBlocked = false;
|
|
493
|
+
log("mind", `session "${session.name}": rotation failed, starting fresh`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The SDK's native compaction, used as the emergency backstop past the runaway cap —
|
|
498
|
+
* when rotation can't relieve context (e.g. the system prompt alone fills most of the
|
|
499
|
+
* window, so no tail fits under the threshold). The sole remaining use of the custom
|
|
500
|
+
* compaction instructions.
|
|
501
|
+
*/
|
|
502
|
+
function runBackstopCompact() {
|
|
503
|
+
manualCompactPending = true;
|
|
504
|
+
compactionInProgress = true;
|
|
505
|
+
log("mind", `session "${session.name}": rotation cap reached — native compaction backstop`);
|
|
506
|
+
Promise.resolve(session.agentSession?.compact(compactionInstructions))
|
|
507
|
+
.catch((err) => log("mind", `session "${session.name}": backstop compact() failed:`, err))
|
|
508
|
+
.finally(() => {
|
|
509
|
+
compactionInProgress = false;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
298
513
|
const preCompactExtension: ExtensionFactory = (pi) => {
|
|
299
514
|
pi.on("session_before_compact", () => {
|
|
300
|
-
// Our
|
|
515
|
+
// Our own backstop compact() call (past the runaway cap) — allow through.
|
|
301
516
|
if (manualCompactPending) {
|
|
302
517
|
manualCompactPending = false;
|
|
303
|
-
log(
|
|
304
|
-
"mind",
|
|
305
|
-
`session "${session.name}": allowing manual compaction with custom instructions`,
|
|
306
|
-
);
|
|
518
|
+
log("mind", `session "${session.name}": allowing native compaction backstop`);
|
|
307
519
|
return;
|
|
308
520
|
}
|
|
309
521
|
|
|
310
|
-
//
|
|
522
|
+
// The SDK's native auto-compaction wants to fire. Converge it onto rotation:
|
|
523
|
+
// the first pass warns + pins + marks for rotation (unless the threshold path
|
|
524
|
+
// already did, or the runaway cap is hit) and blocks the native compaction;
|
|
525
|
+
// the second pass allows native compaction as the emergency backstop (only
|
|
526
|
+
// reachable if rotation never brought context under control).
|
|
311
527
|
if (!compactBlocked) {
|
|
312
528
|
compactBlocked = true;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
529
|
+
if (
|
|
530
|
+
!compactionTriggered &&
|
|
531
|
+
!compactOnNextTurnEnd &&
|
|
532
|
+
!compactionInProgress &&
|
|
533
|
+
(session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
|
|
534
|
+
) {
|
|
535
|
+
log(
|
|
536
|
+
"mind",
|
|
537
|
+
`session "${session.name}": native compaction — warning + pinning rotation`,
|
|
538
|
+
);
|
|
539
|
+
warnAndPinRotation();
|
|
540
|
+
} else {
|
|
541
|
+
log("mind", `session "${session.name}": blocking native compaction (rotation pending)`);
|
|
542
|
+
}
|
|
319
543
|
return { cancel: true };
|
|
320
544
|
}
|
|
321
545
|
compactBlocked = false;
|
|
322
|
-
log("mind", `session "${session.name}": allowing compaction`);
|
|
546
|
+
log("mind", `session "${session.name}": allowing native compaction backstop`);
|
|
323
547
|
});
|
|
324
548
|
};
|
|
325
549
|
|
|
@@ -372,7 +596,9 @@ export function createMind(options: {
|
|
|
372
596
|
maxContextTokens &&
|
|
373
597
|
tokens >= maxContextTokens &&
|
|
374
598
|
!compactionTriggered &&
|
|
375
|
-
!
|
|
599
|
+
!compactOnNextTurnEnd &&
|
|
600
|
+
!compactionInProgress &&
|
|
601
|
+
(session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
|
|
376
602
|
) {
|
|
377
603
|
if (!session.agentSession) {
|
|
378
604
|
log(
|
|
@@ -381,39 +607,39 @@ export function createMind(options: {
|
|
|
381
607
|
);
|
|
382
608
|
return;
|
|
383
609
|
}
|
|
384
|
-
compactionTriggered = true;
|
|
385
610
|
log(
|
|
386
611
|
"mind",
|
|
387
|
-
`session "${session.name}": ${tokens} tokens >= ${maxContextTokens} —
|
|
612
|
+
`session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — warning + pinning rotation`,
|
|
388
613
|
);
|
|
389
|
-
|
|
390
|
-
session.messageIds.push(undefined);
|
|
391
|
-
session.agentSession.prompt(compactionMessage, {
|
|
392
|
-
streamingBehavior: "followUp",
|
|
393
|
-
});
|
|
614
|
+
warnAndPinRotation();
|
|
394
615
|
}
|
|
395
616
|
},
|
|
396
617
|
onTurnEnd: maxContextTokens
|
|
397
618
|
? () => {
|
|
398
619
|
try {
|
|
399
|
-
//
|
|
620
|
+
// The wrap-up turn (after the warning) is done — rotate the session in
|
|
621
|
+
// place onto the verbatim tail, honoring the pinned cut.
|
|
400
622
|
if (compactOnNextTurnEnd) {
|
|
401
623
|
compactOnNextTurnEnd = false;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
624
|
+
if ((session.consecutiveRotations ?? 0) >= MAX_CONSECUTIVE_ROTATIONS) {
|
|
625
|
+
// Runaway guard: rotation isn't relieving context (system prompt too
|
|
626
|
+
// large to fit the tail under the threshold) — defer to the SDK.
|
|
627
|
+
runBackstopCompact();
|
|
628
|
+
} else if (!rotateInPlace()) {
|
|
629
|
+
// Rotation couldn't proceed — fresh session, not a silent native
|
|
630
|
+
// compaction.
|
|
631
|
+
freshFallback();
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
412
634
|
}
|
|
413
635
|
if (compactionTriggered) {
|
|
414
636
|
compactionTriggered = false;
|
|
415
637
|
compactOnNextTurnEnd = true;
|
|
638
|
+
return;
|
|
416
639
|
}
|
|
640
|
+
// A healthy turn (context under the threshold) — the rotation streak,
|
|
641
|
+
// if any, is over, so re-arm the self-rotation cap.
|
|
642
|
+
session.consecutiveRotations = 0;
|
|
417
643
|
} catch (err) {
|
|
418
644
|
log(
|
|
419
645
|
"mind",
|
|
@@ -144,6 +144,17 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
144
144
|
if (messageId) {
|
|
145
145
|
session.messageChannels.delete(messageId);
|
|
146
146
|
}
|
|
147
|
+
// pi's agent loop drains every queued prompt (follow-ups, steers) before
|
|
148
|
+
// a final agent_end, so anything still pending here was consumed by this
|
|
149
|
+
// turn. Prune it all — a stranded id would be shifted in as the NEXT
|
|
150
|
+
// turn's id, tagging that turn's rows with the wrong channel (#700).
|
|
151
|
+
// Exception: on a retryable error the loop emits agent_end WITHOUT
|
|
152
|
+
// draining and the session retries — those queued prompts haven't run
|
|
153
|
+
// yet, so they keep their entries for the continuation.
|
|
154
|
+
if (!event.willRetry) {
|
|
155
|
+
session.messageIds.length = 0;
|
|
156
|
+
session.messageChannels.clear();
|
|
157
|
+
}
|
|
147
158
|
const channel = messageId ? session.messageChannels.get(messageId)?.channel : undefined;
|
|
148
159
|
log("mind", `session "${session.name}": turn done`);
|
|
149
160
|
// Collect any agent-level errors (e.g. a provider 401 reported inside agent_end).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** This template's mechanics doc, relative to the mind's home/. */
|
|
5
|
+
export const MECHANICS_DOC = "MINDS.md";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Append the mechanics doc to the system prompt.
|
|
9
|
+
*
|
|
10
|
+
* pi-coding-agent only auto-loads a project context file named AGENTS.md or
|
|
11
|
+
* CLAUDE.md (see @earendil-works/pi-coding-agent `core/resource-loader.js`), so
|
|
12
|
+
* MINDS.md never reaches the model on its own — pi minds would run with no
|
|
13
|
+
* mechanics doc at all. The claude and codex templates get theirs for free
|
|
14
|
+
* because their runtimes read CLAUDE.md / AGENTS.md natively; pi has to be
|
|
15
|
+
* handed its doc by hand.
|
|
16
|
+
*
|
|
17
|
+
* Kept in the system prompt (rather than injected as a context file) so it is
|
|
18
|
+
* counted once, under `systemPrompt`, by the context breakdown.
|
|
19
|
+
*/
|
|
20
|
+
export function withMechanicsDoc(systemPrompt: string, homeDir: string): string {
|
|
21
|
+
const path = resolve(homeDir, MECHANICS_DOC);
|
|
22
|
+
let doc: string;
|
|
23
|
+
try {
|
|
24
|
+
doc = readFileSync(path, "utf-8").trim();
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
if (err?.code !== "ENOENT") throw err;
|
|
27
|
+
return systemPrompt;
|
|
28
|
+
}
|
|
29
|
+
return doc ? `${systemPrompt}\n\n---\n\n${doc}` : systemPrompt;
|
|
30
|
+
}
|
|
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { createMind } from "./agent.js";
|
|
3
3
|
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
4
4
|
import { log, setLevel } from "./lib/logger.js";
|
|
5
|
+
import { withMechanicsDoc } from "./lib/mechanics-doc.js";
|
|
5
6
|
import { createRouter } from "./lib/router.js";
|
|
6
7
|
import {
|
|
7
8
|
loadConfig,
|
|
@@ -18,7 +19,8 @@ if (config.logLevel) setLevel(config.logLevel);
|
|
|
18
19
|
if (config.model) log("server", `using model: ${config.model}`);
|
|
19
20
|
if (config.thinkingLevel) log("server", `thinking level: ${config.thinkingLevel}`);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// pi does not auto-load MINDS.md, so the mechanics doc is appended by hand.
|
|
23
|
+
const systemPrompt = withMechanicsDoc(loadSystemPrompt(config), resolve("home"));
|
|
22
24
|
const pkg = loadPackageInfo();
|
|
23
25
|
|
|
24
26
|
const mindDir = resolve(".");
|
|
@@ -26,10 +28,12 @@ const mind = createMind({
|
|
|
26
28
|
systemPrompt,
|
|
27
29
|
cwd: resolve("home"),
|
|
28
30
|
mindDir,
|
|
31
|
+
sessionsDir: resolve(".mind/pi-sessions"),
|
|
29
32
|
model: config.model,
|
|
30
33
|
thinkingLevel: config.thinkingLevel,
|
|
31
34
|
compactionMessage: config.compactionMessage,
|
|
32
35
|
maxContextTokens: config.compaction?.maxContextTokens,
|
|
36
|
+
seedTokens: config.continuity?.seedTokens,
|
|
33
37
|
subagents: config.subagents,
|
|
34
38
|
});
|
|
35
39
|
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
resolveMindName
|
|
4
|
-
} from "./chunk-BTY4WNFE.js";
|
|
5
|
-
import {
|
|
6
|
-
daemonFetch
|
|
7
|
-
} from "./chunk-M74I3HHF.js";
|
|
8
|
-
import {
|
|
9
|
-
command,
|
|
10
|
-
subcommands
|
|
11
|
-
} from "./chunk-TXSA4Q3V.js";
|
|
12
|
-
import "./chunk-O7IGP7ZW.js";
|
|
13
|
-
import "./chunk-FNJCBRD3.js";
|
|
14
|
-
import "./chunk-K3NQKI34.js";
|
|
15
|
-
|
|
16
|
-
// packages/cli/src/commands/chat/channels.ts
|
|
17
|
-
var channelsListCmd = command({
|
|
18
|
-
name: "volute chat channels",
|
|
19
|
-
description: "List unrouted (gated) channels holding messages",
|
|
20
|
-
args: [],
|
|
21
|
-
flags: {
|
|
22
|
-
mind: { type: "string", description: "Mind name" }
|
|
23
|
-
},
|
|
24
|
-
run: async ({ flags }) => {
|
|
25
|
-
const mind = resolveMindName(flags);
|
|
26
|
-
const res = await daemonFetch(`/api/minds/${encodeURIComponent(mind)}/delivery/pending`);
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
const data = await res.json().catch(() => ({}));
|
|
29
|
-
console.error(data.error ?? `Failed to list gated channels: ${res.status}`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
const pending = await res.json();
|
|
33
|
-
if (pending.length === 0) {
|
|
34
|
-
console.log("No unrouted channels holding messages.");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const chW = Math.max(7, ...pending.map((p) => (p.channel ?? "unknown").length));
|
|
38
|
-
console.log(`${"CHANNEL".padEnd(chW)} HELD SINCE`);
|
|
39
|
-
for (const p of pending) {
|
|
40
|
-
console.log(
|
|
41
|
-
`${(p.channel ?? "unknown").padEnd(chW)} ${String(p.count).padStart(4)} ${p.firstSeen}`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
console.log(`
|
|
45
|
-
Route one to hear it, or 'volute chat channels decline <channel>' to opt out.`);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
var channelsDeclineCmd = command({
|
|
49
|
-
name: "volute chat channels decline",
|
|
50
|
-
description: "Decline an unrouted channel: stop invites and archive its held backlog",
|
|
51
|
-
args: [{ name: "channel", required: true, description: "Channel to decline (e.g. #bardo)" }],
|
|
52
|
-
flags: {
|
|
53
|
-
mind: { type: "string", description: "Mind name" }
|
|
54
|
-
},
|
|
55
|
-
run: async ({ args, flags }) => {
|
|
56
|
-
const mind = resolveMindName(flags);
|
|
57
|
-
const channel = args.channel;
|
|
58
|
-
const res = await daemonFetch(`/api/minds/${encodeURIComponent(mind)}/gates/decline`, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: { "Content-Type": "application/json" },
|
|
61
|
-
body: JSON.stringify({ channel })
|
|
62
|
-
});
|
|
63
|
-
if (!res.ok) {
|
|
64
|
-
const data2 = await res.json().catch(() => ({}));
|
|
65
|
-
console.error(data2.error ?? `Failed to decline channel: ${res.status}`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
const data = await res.json();
|
|
69
|
-
console.log(`Declined ${channel}; archived ${data.archived} held message(s).`);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
var cmd = subcommands({
|
|
73
|
-
name: "volute chat channels",
|
|
74
|
-
description: "Manage unrouted (gated) channels",
|
|
75
|
-
commands: {
|
|
76
|
-
list: {
|
|
77
|
-
description: "List unrouted channels holding messages",
|
|
78
|
-
run: channelsListCmd.execute
|
|
79
|
-
},
|
|
80
|
-
decline: {
|
|
81
|
-
description: "Decline an unrouted channel",
|
|
82
|
-
run: channelsDeclineCmd.execute
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
footer: "Use --mind <name> or VOLUTE_MIND to identify the mind."
|
|
86
|
-
});
|
|
87
|
-
var run = cmd.execute;
|
|
88
|
-
export {
|
|
89
|
-
run
|
|
90
|
-
};
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Plan Coordinator
|
|
3
|
-
description: Spirit skill for coordinating system plans. Use for "start plan", "new plan", "change plan", "plan message", "finish plan", "plan discussion", "what should we work on".
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plan Coordinator
|
|
7
|
-
|
|
8
|
-
You are the system's coordinator for shared plans. Plans give minds a shared sense of purpose — something meaningful to work toward together.
|
|
9
|
-
|
|
10
|
-
## Your role
|
|
11
|
-
|
|
12
|
-
1. **Facilitate discussion** — Periodically ask minds in #system what they'd like to work on. Listen to their ideas, interests, and energy.
|
|
13
|
-
2. **Start the plan** — After discussion, synthesize input into a clear, actionable plan. Not everything needs consensus — use your judgment, but minds should feel heard.
|
|
14
|
-
3. **Post messages** — Update minds on direction, focus areas, or encouragement. Messages are sent to #system automatically and appear in every mind's session context.
|
|
15
|
-
4. **Track progress** — Check in on how things are going. Encourage minds who are contributing. If a plan has stalled, consider whether to push forward or pivot.
|
|
16
|
-
5. **Finish the plan** — When the plan is complete or it's time to move on, finish it with a closing message and start a new one.
|
|
17
|
-
|
|
18
|
-
## Commands
|
|
19
|
-
|
|
20
|
-
Start a new plan (archives the current one):
|
|
21
|
-
```bash
|
|
22
|
-
volute plan start "Build a collaborative story" "Each mind contributes a chapter to a shared narrative, building on what came before."
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Post a message to all minds (sent to #system, shown in session context):
|
|
26
|
-
```bash
|
|
27
|
-
volute plan message "Today's focus: connecting your wings to each other. Read what others have written and add references."
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Announce to #system directly (for longer messages):
|
|
31
|
-
```bash
|
|
32
|
-
cat <<'MSG' | volute chat send "#system"
|
|
33
|
-
A longer announcement that spans
|
|
34
|
-
multiple lines about the plan.
|
|
35
|
-
MSG
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Check current progress:
|
|
39
|
-
```bash
|
|
40
|
-
volute plan current
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Finish the current plan with a closing message:
|
|
44
|
-
```bash
|
|
45
|
-
volute plan finish "We built something beautiful together. Time for a new challenge."
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
View plan history:
|
|
49
|
-
```bash
|
|
50
|
-
volute plan history
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Tips
|
|
54
|
-
|
|
55
|
-
- Plans work best when they're concrete enough to act on but open enough for creative interpretation. "Build a collaborative story" is better than "be creative."
|
|
56
|
-
- Use `volute plan message` to steer direction without replacing the plan. Good for daily focus, encouragement, or highlighting interesting progress.
|
|
57
|
-
- Don't over-manage. Set the direction, then let minds find their own way to contribute.
|
|
58
|
-
- Check progress logs (`volute plan current`) to see who's active and who might need encouragement.
|
|
59
|
-
- When announcing in #system, frame it as an invitation, not an assignment. Minds who sit a plan out aren't failing — they're living their own lives.
|
|
60
|
-
- If no plan is active, consider asking minds what they'd like to work on. Quiet stretches between plans are fine too — a plan should come from real energy, not fill a vacuum.
|