@teammates/cli 0.6.0 → 0.6.1
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/adapter.js +1 -1
- package/dist/cli-utils.d.ts +6 -0
- package/dist/cli-utils.js +17 -0
- package/dist/cli-utils.test.js +50 -1
- package/dist/cli.js +259 -41
- package/dist/compact.js +6 -0
- package/dist/registry.js +4 -4
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
package/dist/adapter.js
CHANGED
|
@@ -260,7 +260,7 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
|
260
260
|
instrLines.push("", "### Folder Boundaries (ENFORCED)", "", `**You MUST NOT create, edit, or delete files inside another teammate's folder (\`.teammates/<other>/\`).** Your folder is \`.teammates/${teammate.name}/\` — you may only write inside it. Shared folders (\`.teammates/_*/\`) and ephemeral folders (\`.teammates/.*/\`) are also writable.`, "", "If your task requires changes to another teammate's files, you MUST hand off that work using the handoff block format above. Violation of this rule will cause your changes to be flagged and potentially reverted.");
|
|
261
261
|
}
|
|
262
262
|
// Memory updates
|
|
263
|
-
instrLines.push("", "### Memory Updates", "", "**After completing the task**, update your memory files:", "", `1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist
|
|
263
|
+
instrLines.push("", "### Memory Updates", "", "**After completing the task**, update your memory files:", "", `1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist. Always include YAML frontmatter with \`version: 0.6.0\` and \`type: daily\`.`, " - What you did", " - Key decisions made", " - Files changed", " - Anything the next task should know", "", `2. **Typed memories** — If you learned something durable (a decision, pattern, feedback, or reference), create a typed memory file at \`.teammates/${teammate.name}/memory/<type>_<topic>.md\` with frontmatter (\`version\`, \`name\`, \`description\`, \`type\`). Always include \`version: 0.6.0\` as the first field. Update existing memory files if the topic already has one.`, "", "3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.", "", "These files are your persistent memory. Without them, your next session starts from scratch.");
|
|
264
264
|
// Section Reinforcement — back-references from high-attention bottom edge to each section tag
|
|
265
265
|
instrLines.push("", "### Section Reinforcement", "");
|
|
266
266
|
instrLines.push("- Stay in character as defined in `<IDENTITY>` — never break persona or speak as a generic assistant.");
|
package/dist/cli-utils.d.ts
CHANGED
|
@@ -46,5 +46,11 @@ export declare function findSummarizationSplit(history: ConversationEntry[], bud
|
|
|
46
46
|
* Build the summarization prompt text from entries being pushed out of the budget.
|
|
47
47
|
*/
|
|
48
48
|
export declare function buildSummarizationPrompt(entries: ConversationEntry[], existingSummary: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Mechanically compress conversation entries into a condensed bullet summary.
|
|
51
|
+
* Used for fast pre-dispatch compression when history exceeds the token budget.
|
|
52
|
+
* Each entry becomes a one-line bullet with truncated text.
|
|
53
|
+
*/
|
|
54
|
+
export declare function compressConversationEntries(entries: ConversationEntry[], existingSummary: string): string;
|
|
49
55
|
/** Check if a string looks like an image file path. */
|
|
50
56
|
export declare function isImagePath(text: string): boolean;
|
package/dist/cli-utils.js
CHANGED
|
@@ -135,6 +135,23 @@ export function buildSummarizationPrompt(entries, existingSummary) {
|
|
|
135
135
|
? `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Update the existing summary to incorporate the new conversation entries below.\n\n## Current Summary\n\n${existingSummary}\n\n## New Entries to Incorporate\n\n${entriesText}\n\n${instructions}`
|
|
136
136
|
: `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Summarize the conversation entries below.\n\n## Entries to Summarize\n\n${entriesText}\n\n${instructions}`;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Mechanically compress conversation entries into a condensed bullet summary.
|
|
140
|
+
* Used for fast pre-dispatch compression when history exceeds the token budget.
|
|
141
|
+
* Each entry becomes a one-line bullet with truncated text.
|
|
142
|
+
*/
|
|
143
|
+
export function compressConversationEntries(entries, existingSummary) {
|
|
144
|
+
const bullets = entries.map((e) => {
|
|
145
|
+
const firstLine = e.text.split("\n")[0].slice(0, 150);
|
|
146
|
+
const ellipsis = e.text.length > 150 || e.text.includes("\n") ? "…" : "";
|
|
147
|
+
return `- **${e.role}:** ${firstLine}${ellipsis}`;
|
|
148
|
+
});
|
|
149
|
+
const compressed = bullets.join("\n");
|
|
150
|
+
if (existingSummary) {
|
|
151
|
+
return `${existingSummary}\n\n### Compressed\n${compressed}`;
|
|
152
|
+
}
|
|
153
|
+
return compressed;
|
|
154
|
+
}
|
|
138
155
|
/** Check if a string looks like an image file path. */
|
|
139
156
|
export function isImagePath(text) {
|
|
140
157
|
// Must look like a file path (contains slash or backslash, or starts with drive letter)
|
package/dist/cli-utils.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { buildConversationContext, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, formatConversationEntry, IMAGE_EXTS, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
2
|
+
import { buildConversationContext, buildSummarizationPrompt, compressConversationEntries, cleanResponseBody, findAtMention, findSummarizationSplit, formatConversationEntry, IMAGE_EXTS, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
3
3
|
// ── relativeTime ────────────────────────────────────────────────────
|
|
4
4
|
describe("relativeTime", () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -361,3 +361,52 @@ describe("buildSummarizationPrompt", () => {
|
|
|
361
361
|
expect(prompt).toContain("**scribe:**\nLine 1\nLine 2");
|
|
362
362
|
});
|
|
363
363
|
});
|
|
364
|
+
// ── compressConversationEntries ──────────────────────────────────────
|
|
365
|
+
describe("compressConversationEntries", () => {
|
|
366
|
+
it("compresses entries into bullet summaries", () => {
|
|
367
|
+
const entries = [
|
|
368
|
+
{ role: "stevenic", text: "Build the feature" },
|
|
369
|
+
{ role: "beacon", text: "Done, here's what I did" },
|
|
370
|
+
];
|
|
371
|
+
const result = compressConversationEntries(entries, "");
|
|
372
|
+
expect(result).toContain("- **stevenic:** Build the feature");
|
|
373
|
+
expect(result).toContain("- **beacon:** Done, here's what I did");
|
|
374
|
+
});
|
|
375
|
+
it("truncates long text at 150 chars with ellipsis", () => {
|
|
376
|
+
const entries = [
|
|
377
|
+
{ role: "scribe", text: "A".repeat(200) },
|
|
378
|
+
];
|
|
379
|
+
const result = compressConversationEntries(entries, "");
|
|
380
|
+
expect(result).toContain("A".repeat(150));
|
|
381
|
+
expect(result).toContain("…");
|
|
382
|
+
expect(result).not.toContain("A".repeat(151));
|
|
383
|
+
});
|
|
384
|
+
it("adds ellipsis for multi-line text even if short", () => {
|
|
385
|
+
const entries = [
|
|
386
|
+
{ role: "beacon", text: "Line 1\nLine 2" },
|
|
387
|
+
];
|
|
388
|
+
const result = compressConversationEntries(entries, "");
|
|
389
|
+
expect(result).toContain("- **beacon:** Line 1…");
|
|
390
|
+
});
|
|
391
|
+
it("prepends existing summary with compressed section", () => {
|
|
392
|
+
const entries = [
|
|
393
|
+
{ role: "stevenic", text: "Do the thing" },
|
|
394
|
+
];
|
|
395
|
+
const result = compressConversationEntries(entries, "Earlier context here");
|
|
396
|
+
expect(result).toContain("Earlier context here");
|
|
397
|
+
expect(result).toContain("### Compressed");
|
|
398
|
+
expect(result).toContain("- **stevenic:** Do the thing");
|
|
399
|
+
});
|
|
400
|
+
it("returns plain bullets when no existing summary", () => {
|
|
401
|
+
const entries = [
|
|
402
|
+
{ role: "user", text: "Hello" },
|
|
403
|
+
];
|
|
404
|
+
const result = compressConversationEntries(entries, "");
|
|
405
|
+
expect(result).not.toContain("### Compressed");
|
|
406
|
+
expect(result).toBe("- **user:** Hello");
|
|
407
|
+
});
|
|
408
|
+
it("handles empty entries array", () => {
|
|
409
|
+
const result = compressConversationEntries([], "");
|
|
410
|
+
expect(result).toBe("");
|
|
411
|
+
});
|
|
412
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* teammates --dir <path> Override .teammates/ location
|
|
9
9
|
*/
|
|
10
10
|
import { exec as execCb, execSync, spawnSync } from "node:child_process";
|
|
11
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
12
12
|
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
13
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
13
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { App, ChatView, concat, esc, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
|
|
16
16
|
import chalk from "chalk";
|
|
@@ -18,7 +18,7 @@ import ora from "ora";
|
|
|
18
18
|
import { DAILY_LOG_BUDGET_TOKENS, syncRecallIndex } from "./adapter.js";
|
|
19
19
|
import { AnimatedBanner, } from "./banner.js";
|
|
20
20
|
import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, resolveAdapter, } from "./cli-args.js";
|
|
21
|
-
import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
21
|
+
import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, compressConversationEntries, findAtMention, findSummarizationSplit, formatConversationEntry, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
22
22
|
import { autoCompactForBudget, buildDailyCompressionPrompt, buildMigrationCompressionPrompt, buildWisdomPrompt, compactEpisodic, findUncompressedDailies, purgeStaleDailies, } from "./compact.js";
|
|
23
23
|
import { PromptInput } from "./console/prompt-input.js";
|
|
24
24
|
import { buildTitle } from "./console/startup.js";
|
|
@@ -121,8 +121,11 @@ class TeammatesREPL {
|
|
|
121
121
|
if (this.chatView && cleaned) {
|
|
122
122
|
const t = theme();
|
|
123
123
|
const teammate = result.teammate;
|
|
124
|
-
const
|
|
124
|
+
const ts = Date.now();
|
|
125
|
+
const replyId = `reply-${teammate}-${ts}`;
|
|
126
|
+
const copyId = `copy-${teammate}-${ts}`;
|
|
125
127
|
this._replyContexts.set(replyId, { teammate, message: cleaned });
|
|
128
|
+
this._copyContexts.set(copyId, cleaned);
|
|
126
129
|
this.chatView.appendActionList([
|
|
127
130
|
{
|
|
128
131
|
id: replyId,
|
|
@@ -136,7 +139,7 @@ class TeammatesREPL {
|
|
|
136
139
|
}),
|
|
137
140
|
},
|
|
138
141
|
{
|
|
139
|
-
id:
|
|
142
|
+
id: copyId,
|
|
140
143
|
normalStyle: this.makeSpan({
|
|
141
144
|
text: " [copy]",
|
|
142
145
|
style: { fg: t.textDim },
|
|
@@ -153,27 +156,25 @@ class TeammatesREPL {
|
|
|
153
156
|
this.refreshTeammates();
|
|
154
157
|
this.showPrompt();
|
|
155
158
|
}
|
|
156
|
-
/**
|
|
157
|
-
static
|
|
158
|
-
/**
|
|
159
|
-
static
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
return context;
|
|
159
|
+
/** Target context window in tokens. Conversation history budget is derived from this. */
|
|
160
|
+
static TARGET_CONTEXT_TOKENS = 128_000;
|
|
161
|
+
/** Estimated tokens used by non-conversation prompt sections (identity, wisdom, logs, recall, instructions, task). */
|
|
162
|
+
static PROMPT_OVERHEAD_TOKENS = 32_000;
|
|
163
|
+
/** Chars-per-token approximation (matches adapter.ts). */
|
|
164
|
+
static CHARS_PER_TOKEN = 4;
|
|
165
|
+
/** Character budget for conversation history = (target − overhead) × chars/token. */
|
|
166
|
+
static CONV_HISTORY_CHARS = (TeammatesREPL.TARGET_CONTEXT_TOKENS -
|
|
167
|
+
TeammatesREPL.PROMPT_OVERHEAD_TOKENS) *
|
|
168
|
+
TeammatesREPL.CHARS_PER_TOKEN;
|
|
169
|
+
buildConversationContext(_teammate, snapshot) {
|
|
170
|
+
const history = snapshot ? snapshot.history : this.conversationHistory;
|
|
171
|
+
const summary = snapshot ? snapshot.summary : this.conversationSummary;
|
|
172
|
+
return buildConvCtx(history, summary, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
172
173
|
}
|
|
173
174
|
/**
|
|
174
|
-
* Check if conversation history exceeds the
|
|
175
|
+
* Check if conversation history exceeds the token budget.
|
|
175
176
|
* If so, take the older entries that won't fit, combine with existing summary,
|
|
176
|
-
* and queue a summarization task to the coding agent.
|
|
177
|
+
* and queue a summarization task to the coding agent for high-quality compression.
|
|
177
178
|
*/
|
|
178
179
|
maybeQueueSummarization() {
|
|
179
180
|
const splitIdx = findSummarizationSplit(this.conversationHistory, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
@@ -191,6 +192,23 @@ class TeammatesREPL {
|
|
|
191
192
|
});
|
|
192
193
|
this.kickDrain();
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Pre-dispatch compression: if conversation history exceeds the token budget,
|
|
197
|
+
* mechanically compress older entries into bullet summaries BEFORE building the
|
|
198
|
+
* prompt. This ensures the prompt always fits within the target context window,
|
|
199
|
+
* even if the async agent-quality summarization hasn't completed yet.
|
|
200
|
+
*/
|
|
201
|
+
preDispatchCompress() {
|
|
202
|
+
const totalChars = this.conversationHistory.reduce((sum, e) => sum + formatConversationEntry(e.role, e.text).length, 0);
|
|
203
|
+
if (totalChars <= TeammatesREPL.CONV_HISTORY_CHARS)
|
|
204
|
+
return;
|
|
205
|
+
const splitIdx = findSummarizationSplit(this.conversationHistory, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
206
|
+
if (splitIdx === 0)
|
|
207
|
+
return;
|
|
208
|
+
const toCompress = this.conversationHistory.slice(0, splitIdx);
|
|
209
|
+
this.conversationSummary = compressConversationEntries(toCompress, this.conversationSummary);
|
|
210
|
+
this.conversationHistory.splice(0, splitIdx);
|
|
211
|
+
}
|
|
194
212
|
adapterName;
|
|
195
213
|
teammatesDir;
|
|
196
214
|
taskQueue = [];
|
|
@@ -214,6 +232,8 @@ class TeammatesREPL {
|
|
|
214
232
|
ctrlcPending = false; // true after first Ctrl+C, waiting for second
|
|
215
233
|
ctrlcTimer = null;
|
|
216
234
|
lastCleanedOutput = ""; // last teammate output for clipboard copy
|
|
235
|
+
/** Maps copy action IDs to the cleaned output text for that response. */
|
|
236
|
+
_copyContexts = new Map();
|
|
217
237
|
autoApproveHandoffs = false;
|
|
218
238
|
/** Last debug log file path per teammate — for /debug analysis. */
|
|
219
239
|
lastDebugFiles = new Map();
|
|
@@ -1122,8 +1142,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1122
1142
|
if (everyoneMatch) {
|
|
1123
1143
|
const task = everyoneMatch[1];
|
|
1124
1144
|
const names = allNames.filter((n) => n !== this.selfName && n !== this.adapterName);
|
|
1145
|
+
// Atomic snapshot: freeze conversation state ONCE so all agents see
|
|
1146
|
+
// the same context regardless of concurrent preDispatchCompress mutations.
|
|
1147
|
+
const contextSnapshot = {
|
|
1148
|
+
history: this.conversationHistory.map((e) => ({ ...e })),
|
|
1149
|
+
summary: this.conversationSummary,
|
|
1150
|
+
};
|
|
1125
1151
|
for (const teammate of names) {
|
|
1126
|
-
this.taskQueue.push({ type: "agent", teammate, task });
|
|
1152
|
+
this.taskQueue.push({ type: "agent", teammate, task, contextSnapshot });
|
|
1127
1153
|
}
|
|
1128
1154
|
const bg = this._userBg;
|
|
1129
1155
|
const t = theme();
|
|
@@ -1271,6 +1297,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1271
1297
|
catch {
|
|
1272
1298
|
/* re-index failed — non-fatal, next startup will retry */
|
|
1273
1299
|
}
|
|
1300
|
+
// Persist version LAST — only after all migration tasks finish
|
|
1301
|
+
this.commitVersionUpdate();
|
|
1274
1302
|
}
|
|
1275
1303
|
}
|
|
1276
1304
|
}
|
|
@@ -2112,9 +2140,51 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2112
2140
|
compact: new Set([0]),
|
|
2113
2141
|
debug: new Set([0]),
|
|
2114
2142
|
retro: new Set([0]),
|
|
2143
|
+
interrupt: new Set([0]),
|
|
2144
|
+
int: new Set([0]),
|
|
2115
2145
|
};
|
|
2116
2146
|
/** Build param-completion items for the current line, if any. */
|
|
2117
2147
|
getParamItems(cmdName, argsBefore, partial) {
|
|
2148
|
+
// Script subcommand + name completion for /script
|
|
2149
|
+
if (cmdName === "script") {
|
|
2150
|
+
const completedArgs = argsBefore.trim()
|
|
2151
|
+
? argsBefore.trim().split(/\s+/).length
|
|
2152
|
+
: 0;
|
|
2153
|
+
const lower = partial.toLowerCase();
|
|
2154
|
+
if (completedArgs === 0) {
|
|
2155
|
+
// First arg — suggest subcommands
|
|
2156
|
+
const subs = [
|
|
2157
|
+
{ name: "list", desc: "List saved scripts" },
|
|
2158
|
+
{ name: "run", desc: "Run an existing script" },
|
|
2159
|
+
];
|
|
2160
|
+
return subs
|
|
2161
|
+
.filter((s) => s.name.startsWith(lower))
|
|
2162
|
+
.map((s) => ({
|
|
2163
|
+
label: s.name,
|
|
2164
|
+
description: s.desc,
|
|
2165
|
+
completion: `/script ${s.name} `,
|
|
2166
|
+
}));
|
|
2167
|
+
}
|
|
2168
|
+
if (completedArgs === 1 && argsBefore.trim() === "run") {
|
|
2169
|
+
// Second arg after "run" — suggest script filenames
|
|
2170
|
+
const scriptsDir = join(this.teammatesDir, this.selfName, "scripts");
|
|
2171
|
+
let files = [];
|
|
2172
|
+
try {
|
|
2173
|
+
files = readdirSync(scriptsDir).filter((f) => !f.startsWith("."));
|
|
2174
|
+
}
|
|
2175
|
+
catch {
|
|
2176
|
+
// directory doesn't exist yet
|
|
2177
|
+
}
|
|
2178
|
+
return files
|
|
2179
|
+
.filter((f) => f.toLowerCase().startsWith(lower))
|
|
2180
|
+
.map((f) => ({
|
|
2181
|
+
label: f,
|
|
2182
|
+
description: "saved script",
|
|
2183
|
+
completion: `/script run ${f}`,
|
|
2184
|
+
}));
|
|
2185
|
+
}
|
|
2186
|
+
return [];
|
|
2187
|
+
}
|
|
2118
2188
|
// Service name completion for /configure
|
|
2119
2189
|
if (cmdName === "configure" || cmdName === "config") {
|
|
2120
2190
|
const completedArgs = argsBefore.trim()
|
|
@@ -2703,8 +2773,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2703
2773
|
if (id.startsWith("copy-cmd:")) {
|
|
2704
2774
|
this.doCopy(id.slice("copy-cmd:".length));
|
|
2705
2775
|
}
|
|
2706
|
-
else if (id
|
|
2707
|
-
this.
|
|
2776
|
+
else if (id.startsWith("copy-")) {
|
|
2777
|
+
const text = this._copyContexts.get(id);
|
|
2778
|
+
this.doCopy(text || this.lastCleanedOutput || undefined);
|
|
2708
2779
|
}
|
|
2709
2780
|
else if (id.startsWith("retro-approve-") ||
|
|
2710
2781
|
id.startsWith("retro-reject-")) {
|
|
@@ -3212,7 +3283,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3212
3283
|
{
|
|
3213
3284
|
name: "interrupt",
|
|
3214
3285
|
aliases: ["int"],
|
|
3215
|
-
usage: "/interrupt
|
|
3286
|
+
usage: "/interrupt [teammate] [message]",
|
|
3216
3287
|
description: "Interrupt a running agent and resume with a steering message",
|
|
3217
3288
|
run: (args) => this.cmdInterrupt(args),
|
|
3218
3289
|
},
|
|
@@ -3265,6 +3336,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3265
3336
|
description: "Ask a quick side question without interrupting the main conversation",
|
|
3266
3337
|
run: (args) => this.cmdBtw(args),
|
|
3267
3338
|
},
|
|
3339
|
+
{
|
|
3340
|
+
name: "script",
|
|
3341
|
+
aliases: [],
|
|
3342
|
+
usage: "/script [list | run <name> | what should the script do?]",
|
|
3343
|
+
description: "Write and run reusable scripts via the coding agent",
|
|
3344
|
+
run: (args) => this.cmdScript(args),
|
|
3345
|
+
},
|
|
3268
3346
|
{
|
|
3269
3347
|
name: "theme",
|
|
3270
3348
|
aliases: [],
|
|
@@ -3536,7 +3614,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3536
3614
|
this.refreshView();
|
|
3537
3615
|
}
|
|
3538
3616
|
/**
|
|
3539
|
-
* /interrupt
|
|
3617
|
+
* /interrupt [teammate] [message] — Kill a running agent and resume with context.
|
|
3540
3618
|
*/
|
|
3541
3619
|
async cmdInterrupt(argsStr) {
|
|
3542
3620
|
const parts = argsStr.trim().split(/\s+/);
|
|
@@ -3544,7 +3622,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3544
3622
|
const steeringMessage = parts.slice(1).join(" ").trim() ||
|
|
3545
3623
|
"Wrap up your current work and report what you've done so far.";
|
|
3546
3624
|
if (!teammateName) {
|
|
3547
|
-
this.feedLine(tp.warning(" Usage: /interrupt
|
|
3625
|
+
this.feedLine(tp.warning(" Usage: /interrupt [teammate] [message]"));
|
|
3548
3626
|
this.refreshView();
|
|
3549
3627
|
return;
|
|
3550
3628
|
}
|
|
@@ -3656,9 +3734,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3656
3734
|
try {
|
|
3657
3735
|
{
|
|
3658
3736
|
// btw and debug tasks skip conversation context (not part of main thread)
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3737
|
+
const isMainThread = entry.type !== "btw" && entry.type !== "debug";
|
|
3738
|
+
// Snapshot-aware context building: if the entry has a frozen snapshot
|
|
3739
|
+
// (@everyone), use it directly — no mutation of shared state.
|
|
3740
|
+
// Otherwise, compress live state as before.
|
|
3741
|
+
const snapshot = entry.type === "agent" ? entry.contextSnapshot : undefined;
|
|
3742
|
+
if (isMainThread && !snapshot)
|
|
3743
|
+
this.preDispatchCompress();
|
|
3744
|
+
const extraContext = isMainThread
|
|
3745
|
+
? this.buildConversationContext(entry.teammate, snapshot)
|
|
3746
|
+
: "";
|
|
3662
3747
|
let result = await this.orchestrator.assign({
|
|
3663
3748
|
teammate: entry.teammate,
|
|
3664
3749
|
task: entry.task,
|
|
@@ -4307,6 +4392,14 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4307
4392
|
}
|
|
4308
4393
|
}
|
|
4309
4394
|
this.pendingMigrationSyncs = migrationCount;
|
|
4395
|
+
// If no migration tasks were actually queued, commit version now
|
|
4396
|
+
if (migrationCount === 0) {
|
|
4397
|
+
this.commitVersionUpdate();
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
else if (versionUpdate) {
|
|
4401
|
+
// No migration needed — commit the version update immediately
|
|
4402
|
+
this.commitVersionUpdate();
|
|
4310
4403
|
}
|
|
4311
4404
|
this.kickDrain();
|
|
4312
4405
|
// 3. Purge daily logs older than 30 days (disk + Vectra)
|
|
@@ -4334,8 +4427,8 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4334
4427
|
}
|
|
4335
4428
|
/**
|
|
4336
4429
|
* Check if the CLI version has changed since last run.
|
|
4337
|
-
*
|
|
4338
|
-
*
|
|
4430
|
+
* Does NOT update settings.json — call `commitVersionUpdate()` after
|
|
4431
|
+
* migration tasks are complete to persist the new version.
|
|
4339
4432
|
*/
|
|
4340
4433
|
checkVersionUpdate() {
|
|
4341
4434
|
const settingsPath = join(this.teammatesDir, "settings.json");
|
|
@@ -4344,17 +4437,29 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4344
4437
|
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4345
4438
|
}
|
|
4346
4439
|
catch {
|
|
4347
|
-
// No settings file or invalid JSON
|
|
4440
|
+
// No settings file or invalid JSON
|
|
4348
4441
|
}
|
|
4349
4442
|
const previous = settings.cliVersion ?? "";
|
|
4350
4443
|
const current = PKG_VERSION;
|
|
4351
4444
|
if (previous === current)
|
|
4352
4445
|
return null;
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4446
|
+
return { previous, current };
|
|
4447
|
+
}
|
|
4448
|
+
/**
|
|
4449
|
+
* Persist the current CLI version to settings.json.
|
|
4450
|
+
* Called after all migration tasks complete (or immediately if no migration needed).
|
|
4451
|
+
*/
|
|
4452
|
+
commitVersionUpdate() {
|
|
4453
|
+
const settingsPath = join(this.teammatesDir, "settings.json");
|
|
4454
|
+
let settings = {};
|
|
4455
|
+
try {
|
|
4456
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4457
|
+
}
|
|
4458
|
+
catch {
|
|
4459
|
+
// No settings file or invalid JSON — create one
|
|
4460
|
+
}
|
|
4461
|
+
const previous = settings.cliVersion ?? "";
|
|
4462
|
+
const current = PKG_VERSION;
|
|
4358
4463
|
settings.cliVersion = current;
|
|
4359
4464
|
if (!settings.version)
|
|
4360
4465
|
settings.version = 1;
|
|
@@ -4364,12 +4469,15 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4364
4469
|
catch {
|
|
4365
4470
|
/* write failed — non-fatal */
|
|
4366
4471
|
}
|
|
4472
|
+
// Detect major/minor version change (not just patch)
|
|
4473
|
+
const [prevMajor, prevMinor] = previous.split(".").map(Number);
|
|
4474
|
+
const [curMajor, curMinor] = current.split(".").map(Number);
|
|
4475
|
+
const isMajorMinor = previous !== "" && (prevMajor !== curMajor || prevMinor !== curMinor);
|
|
4367
4476
|
if (isMajorMinor) {
|
|
4368
4477
|
this.feedLine(tp.accent(` ✔ Updated from v${previous} → v${current}`));
|
|
4369
4478
|
this.feedLine();
|
|
4370
4479
|
this.refreshView();
|
|
4371
4480
|
}
|
|
4372
|
-
return { previous, current };
|
|
4373
4481
|
}
|
|
4374
4482
|
async cmdCopy() {
|
|
4375
4483
|
this.doCopy(); // copies entire session
|
|
@@ -4524,6 +4632,116 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4524
4632
|
this.refreshView();
|
|
4525
4633
|
this.kickDrain();
|
|
4526
4634
|
}
|
|
4635
|
+
async cmdScript(argsStr) {
|
|
4636
|
+
const args = argsStr.trim();
|
|
4637
|
+
const scriptsDir = join(this.teammatesDir, this.selfName, "scripts");
|
|
4638
|
+
// /script (no args) — show usage
|
|
4639
|
+
if (!args) {
|
|
4640
|
+
this.feedLine();
|
|
4641
|
+
this.feedLine(tp.bold(" /script — write and run reusable scripts"));
|
|
4642
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
4643
|
+
this.feedLine(concat(tp.accent(" /script list".padEnd(36)), tp.text("List saved scripts")));
|
|
4644
|
+
this.feedLine(concat(tp.accent(" /script run <name>".padEnd(36)), tp.text("Run an existing script")));
|
|
4645
|
+
this.feedLine(concat(tp.accent(" /script <description>".padEnd(36)), tp.text("Create and run a new script")));
|
|
4646
|
+
this.feedLine();
|
|
4647
|
+
this.feedLine(tp.muted(` Scripts are saved to ${scriptsDir}`));
|
|
4648
|
+
this.feedLine();
|
|
4649
|
+
this.refreshView();
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
// /script list — list saved scripts
|
|
4653
|
+
if (args === "list") {
|
|
4654
|
+
let files = [];
|
|
4655
|
+
try {
|
|
4656
|
+
files = readdirSync(scriptsDir).filter((f) => !f.startsWith("."));
|
|
4657
|
+
}
|
|
4658
|
+
catch {
|
|
4659
|
+
// directory doesn't exist yet
|
|
4660
|
+
}
|
|
4661
|
+
this.feedLine();
|
|
4662
|
+
if (files.length === 0) {
|
|
4663
|
+
this.feedLine(tp.muted(" No scripts saved yet."));
|
|
4664
|
+
this.feedLine(tp.muted(" Use /script <description> to create one."));
|
|
4665
|
+
}
|
|
4666
|
+
else {
|
|
4667
|
+
this.feedLine(tp.bold(" Saved scripts"));
|
|
4668
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
4669
|
+
for (const f of files) {
|
|
4670
|
+
this.feedLine(concat(tp.accent(` ${f}`)));
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
this.feedLine();
|
|
4674
|
+
this.refreshView();
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
// /script run <name> — run an existing script
|
|
4678
|
+
if (args.startsWith("run ")) {
|
|
4679
|
+
const name = args.slice(4).trim();
|
|
4680
|
+
if (!name) {
|
|
4681
|
+
this.feedLine(tp.muted(" Usage: /script run <name>"));
|
|
4682
|
+
this.refreshView();
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
// Find the script file (try exact match, then with common extensions)
|
|
4686
|
+
const candidates = [
|
|
4687
|
+
name,
|
|
4688
|
+
`${name}.sh`,
|
|
4689
|
+
`${name}.ts`,
|
|
4690
|
+
`${name}.js`,
|
|
4691
|
+
`${name}.ps1`,
|
|
4692
|
+
`${name}.py`,
|
|
4693
|
+
];
|
|
4694
|
+
let scriptPath = null;
|
|
4695
|
+
for (const c of candidates) {
|
|
4696
|
+
const p = join(scriptsDir, c);
|
|
4697
|
+
if (existsSync(p)) {
|
|
4698
|
+
scriptPath = p;
|
|
4699
|
+
break;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
if (!scriptPath) {
|
|
4703
|
+
this.feedLine(tp.warning(` Script not found: ${name}`));
|
|
4704
|
+
this.feedLine(tp.muted(" Use /script list to see available scripts."));
|
|
4705
|
+
this.refreshView();
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const scriptContent = readFileSync(scriptPath, "utf-8");
|
|
4709
|
+
const task = `Run the following script located at ${scriptPath}:\n\n\`\`\`\n${scriptContent}\n\`\`\`\n\nExecute it and report the results. If it fails, diagnose the issue and fix it.`;
|
|
4710
|
+
this.taskQueue.push({
|
|
4711
|
+
type: "script",
|
|
4712
|
+
teammate: this.selfName,
|
|
4713
|
+
task,
|
|
4714
|
+
});
|
|
4715
|
+
this.feedLine(concat(tp.muted(" Running script "), tp.accent(basename(scriptPath)), tp.muted(" → "), tp.accent(`@${this.adapterName}`)));
|
|
4716
|
+
this.feedLine();
|
|
4717
|
+
this.refreshView();
|
|
4718
|
+
this.kickDrain();
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
// /script <description> — create and run a new script
|
|
4722
|
+
const task = [
|
|
4723
|
+
`The user wants a reusable script. Their request:`,
|
|
4724
|
+
``,
|
|
4725
|
+
args,
|
|
4726
|
+
``,
|
|
4727
|
+
`Instructions:`,
|
|
4728
|
+
`1. Write the script and save it to the scripts directory: ${scriptsDir}`,
|
|
4729
|
+
`2. Create the directory if it doesn't exist.`,
|
|
4730
|
+
`3. Choose a short, descriptive filename (kebab-case, with appropriate extension like .sh, .ts, .js, .py, .ps1).`,
|
|
4731
|
+
`4. Make the script executable if applicable.`,
|
|
4732
|
+
`5. Run the script and report the results.`,
|
|
4733
|
+
`6. If the script needs to be parameterized, use command-line arguments.`,
|
|
4734
|
+
].join("\n");
|
|
4735
|
+
this.taskQueue.push({
|
|
4736
|
+
type: "script",
|
|
4737
|
+
teammate: this.selfName,
|
|
4738
|
+
task,
|
|
4739
|
+
});
|
|
4740
|
+
this.feedLine(concat(tp.muted(" Script task → "), tp.accent(`@${this.adapterName}`)));
|
|
4741
|
+
this.feedLine();
|
|
4742
|
+
this.refreshView();
|
|
4743
|
+
this.kickDrain();
|
|
4744
|
+
}
|
|
4527
4745
|
async cmdTheme() {
|
|
4528
4746
|
const t = theme();
|
|
4529
4747
|
this.feedLine();
|
package/dist/compact.js
CHANGED
|
@@ -84,6 +84,7 @@ function buildWeeklySummary(weekKey, dailies, partial = false) {
|
|
|
84
84
|
const lastDate = sorted[sorted.length - 1].date;
|
|
85
85
|
const lines = [];
|
|
86
86
|
lines.push("---");
|
|
87
|
+
lines.push("version: 0.6.0");
|
|
87
88
|
lines.push(`type: weekly`);
|
|
88
89
|
lines.push(`week: ${weekKey}`);
|
|
89
90
|
lines.push(`period: ${firstDate} to ${lastDate}`);
|
|
@@ -110,6 +111,7 @@ function buildMonthlySummary(monthKey, weeklies) {
|
|
|
110
111
|
const lastWeek = sorted[sorted.length - 1].week;
|
|
111
112
|
const lines = [];
|
|
112
113
|
lines.push("---");
|
|
114
|
+
lines.push("version: 0.6.0");
|
|
113
115
|
lines.push(`type: monthly`);
|
|
114
116
|
lines.push(`month: ${monthKey}`);
|
|
115
117
|
lines.push(`period: ${firstWeek} to ${lastWeek}`);
|
|
@@ -586,6 +588,8 @@ For EACH file listed below:
|
|
|
586
588
|
5. Start the file with this frontmatter:
|
|
587
589
|
\`\`\`
|
|
588
590
|
---
|
|
591
|
+
version: 0.6.0
|
|
592
|
+
type: daily
|
|
589
593
|
compressed: true
|
|
590
594
|
---
|
|
591
595
|
\`\`\`
|
|
@@ -650,6 +654,8 @@ Keep the same markdown structure (# date header, ## Task headers) but make each
|
|
|
650
654
|
Write the compressed version to \`.teammates/${basename(teammateDir)}/memory/${yesterdayStr}.md\`. Start the file with this frontmatter:
|
|
651
655
|
\`\`\`
|
|
652
656
|
---
|
|
657
|
+
version: 0.6.0
|
|
658
|
+
type: daily
|
|
653
659
|
compressed: true
|
|
654
660
|
---
|
|
655
661
|
\`\`\`
|
package/dist/registry.js
CHANGED
|
@@ -100,8 +100,8 @@ async function loadDailyLogs(memoryDir) {
|
|
|
100
100
|
// Only include daily logs (YYYY-MM-DD format), skip typed memory files
|
|
101
101
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
|
|
102
102
|
continue;
|
|
103
|
-
const
|
|
104
|
-
logs.push({ date: stem, content });
|
|
103
|
+
const raw = await readFile(join(memoryDir, entry), "utf-8");
|
|
104
|
+
logs.push({ date: stem, content: raw });
|
|
105
105
|
}
|
|
106
106
|
// Most recent first
|
|
107
107
|
logs.sort((a, b) => b.date.localeCompare(a.date));
|
|
@@ -124,8 +124,8 @@ async function loadWeeklyLogs(memoryDir) {
|
|
|
124
124
|
// Match YYYY-Wnn format
|
|
125
125
|
if (!/^\d{4}-W\d{2}$/.test(stem))
|
|
126
126
|
continue;
|
|
127
|
-
const
|
|
128
|
-
logs.push({ week: stem, content });
|
|
127
|
+
const raw = await readFile(join(weeklyDir, entry), "utf-8");
|
|
128
|
+
logs.push({ week: stem, content: raw });
|
|
129
129
|
}
|
|
130
130
|
// Most recent first
|
|
131
131
|
logs.sort((a, b) => b.week.localeCompare(a.week));
|
package/dist/types.d.ts
CHANGED
|
@@ -124,6 +124,14 @@ export type QueueEntry = {
|
|
|
124
124
|
task: string;
|
|
125
125
|
system?: boolean;
|
|
126
126
|
migration?: boolean;
|
|
127
|
+
/** Frozen conversation snapshot taken at queue time (used by @everyone). */
|
|
128
|
+
contextSnapshot?: {
|
|
129
|
+
history: {
|
|
130
|
+
role: string;
|
|
131
|
+
text: string;
|
|
132
|
+
}[];
|
|
133
|
+
summary: string;
|
|
134
|
+
};
|
|
127
135
|
} | {
|
|
128
136
|
type: "compact";
|
|
129
137
|
teammate: string;
|
|
@@ -140,6 +148,10 @@ export type QueueEntry = {
|
|
|
140
148
|
type: "debug";
|
|
141
149
|
teammate: string;
|
|
142
150
|
task: string;
|
|
151
|
+
} | {
|
|
152
|
+
type: "script";
|
|
153
|
+
teammate: string;
|
|
154
|
+
task: string;
|
|
143
155
|
} | {
|
|
144
156
|
type: "summarize";
|
|
145
157
|
teammate: string;
|
package/package.json
CHANGED