@teammates/cli 0.6.0 → 0.6.2
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 +265 -46
- package/dist/compact.js +6 -0
- package/dist/onboard.js +18 -12
- 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();
|
|
@@ -284,11 +304,12 @@ class TeammatesREPL {
|
|
|
284
304
|
this.statusFrame = 0;
|
|
285
305
|
this.statusRotateIndex = 0;
|
|
286
306
|
this.renderStatusFrame();
|
|
287
|
-
// Animate spinner at ~
|
|
307
|
+
// Animate spinner at ~200ms (fast enough for visual smoothness,
|
|
308
|
+
// slow enough to avoid saturating the render loop under load)
|
|
288
309
|
this.statusTimer = setInterval(() => {
|
|
289
310
|
this.statusFrame++;
|
|
290
311
|
this.renderStatusFrame();
|
|
291
|
-
},
|
|
312
|
+
}, 200);
|
|
292
313
|
// Rotate through teammates every 3 seconds
|
|
293
314
|
this.statusRotateTimer = setInterval(() => {
|
|
294
315
|
if (this.activeTasks.size > 1) {
|
|
@@ -375,7 +396,7 @@ class TeammatesREPL {
|
|
|
375
396
|
: cleanTask;
|
|
376
397
|
if (this.chatView) {
|
|
377
398
|
this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}... `), tp.muted(`${taskText}${suffix}`)));
|
|
378
|
-
this.app.
|
|
399
|
+
this.app.scheduleRefresh();
|
|
379
400
|
}
|
|
380
401
|
else {
|
|
381
402
|
const spinColor = this.statusFrame % 8 === 0 ? chalk.blue : chalk.blueBright;
|
|
@@ -1122,8 +1143,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1122
1143
|
if (everyoneMatch) {
|
|
1123
1144
|
const task = everyoneMatch[1];
|
|
1124
1145
|
const names = allNames.filter((n) => n !== this.selfName && n !== this.adapterName);
|
|
1146
|
+
// Atomic snapshot: freeze conversation state ONCE so all agents see
|
|
1147
|
+
// the same context regardless of concurrent preDispatchCompress mutations.
|
|
1148
|
+
const contextSnapshot = {
|
|
1149
|
+
history: this.conversationHistory.map((e) => ({ ...e })),
|
|
1150
|
+
summary: this.conversationSummary,
|
|
1151
|
+
};
|
|
1125
1152
|
for (const teammate of names) {
|
|
1126
|
-
this.taskQueue.push({ type: "agent", teammate, task });
|
|
1153
|
+
this.taskQueue.push({ type: "agent", teammate, task, contextSnapshot });
|
|
1127
1154
|
}
|
|
1128
1155
|
const bg = this._userBg;
|
|
1129
1156
|
const t = theme();
|
|
@@ -1271,6 +1298,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1271
1298
|
catch {
|
|
1272
1299
|
/* re-index failed — non-fatal, next startup will retry */
|
|
1273
1300
|
}
|
|
1301
|
+
// Persist version LAST — only after all migration tasks finish
|
|
1302
|
+
this.commitVersionUpdate();
|
|
1274
1303
|
}
|
|
1275
1304
|
}
|
|
1276
1305
|
}
|
|
@@ -1709,8 +1738,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1709
1738
|
const userMdPath = join(teammatesDir, "USER.md");
|
|
1710
1739
|
try {
|
|
1711
1740
|
const content = readFileSync(userMdPath, "utf-8");
|
|
1712
|
-
// Template placeholders contain "<
|
|
1713
|
-
return !content.trim() || content.includes("<your name>");
|
|
1741
|
+
// Template placeholders contain "<Your name>" — treat as not set up
|
|
1742
|
+
return !content.trim() || content.toLowerCase().includes("<your name>");
|
|
1714
1743
|
}
|
|
1715
1744
|
catch {
|
|
1716
1745
|
// File doesn't exist
|
|
@@ -2112,9 +2141,51 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2112
2141
|
compact: new Set([0]),
|
|
2113
2142
|
debug: new Set([0]),
|
|
2114
2143
|
retro: new Set([0]),
|
|
2144
|
+
interrupt: new Set([0]),
|
|
2145
|
+
int: new Set([0]),
|
|
2115
2146
|
};
|
|
2116
2147
|
/** Build param-completion items for the current line, if any. */
|
|
2117
2148
|
getParamItems(cmdName, argsBefore, partial) {
|
|
2149
|
+
// Script subcommand + name completion for /script
|
|
2150
|
+
if (cmdName === "script") {
|
|
2151
|
+
const completedArgs = argsBefore.trim()
|
|
2152
|
+
? argsBefore.trim().split(/\s+/).length
|
|
2153
|
+
: 0;
|
|
2154
|
+
const lower = partial.toLowerCase();
|
|
2155
|
+
if (completedArgs === 0) {
|
|
2156
|
+
// First arg — suggest subcommands
|
|
2157
|
+
const subs = [
|
|
2158
|
+
{ name: "list", desc: "List saved scripts" },
|
|
2159
|
+
{ name: "run", desc: "Run an existing script" },
|
|
2160
|
+
];
|
|
2161
|
+
return subs
|
|
2162
|
+
.filter((s) => s.name.startsWith(lower))
|
|
2163
|
+
.map((s) => ({
|
|
2164
|
+
label: s.name,
|
|
2165
|
+
description: s.desc,
|
|
2166
|
+
completion: `/script ${s.name} `,
|
|
2167
|
+
}));
|
|
2168
|
+
}
|
|
2169
|
+
if (completedArgs === 1 && argsBefore.trim() === "run") {
|
|
2170
|
+
// Second arg after "run" — suggest script filenames
|
|
2171
|
+
const scriptsDir = join(this.teammatesDir, this.selfName, "scripts");
|
|
2172
|
+
let files = [];
|
|
2173
|
+
try {
|
|
2174
|
+
files = readdirSync(scriptsDir).filter((f) => !f.startsWith("."));
|
|
2175
|
+
}
|
|
2176
|
+
catch {
|
|
2177
|
+
// directory doesn't exist yet
|
|
2178
|
+
}
|
|
2179
|
+
return files
|
|
2180
|
+
.filter((f) => f.toLowerCase().startsWith(lower))
|
|
2181
|
+
.map((f) => ({
|
|
2182
|
+
label: f,
|
|
2183
|
+
description: "saved script",
|
|
2184
|
+
completion: `/script run ${f}`,
|
|
2185
|
+
}));
|
|
2186
|
+
}
|
|
2187
|
+
return [];
|
|
2188
|
+
}
|
|
2118
2189
|
// Service name completion for /configure
|
|
2119
2190
|
if (cmdName === "configure" || cmdName === "config") {
|
|
2120
2191
|
const completedArgs = argsBefore.trim()
|
|
@@ -2703,8 +2774,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2703
2774
|
if (id.startsWith("copy-cmd:")) {
|
|
2704
2775
|
this.doCopy(id.slice("copy-cmd:".length));
|
|
2705
2776
|
}
|
|
2706
|
-
else if (id
|
|
2707
|
-
this.
|
|
2777
|
+
else if (id.startsWith("copy-")) {
|
|
2778
|
+
const text = this._copyContexts.get(id);
|
|
2779
|
+
this.doCopy(text || this.lastCleanedOutput || undefined);
|
|
2708
2780
|
}
|
|
2709
2781
|
else if (id.startsWith("retro-approve-") ||
|
|
2710
2782
|
id.startsWith("retro-reject-")) {
|
|
@@ -3212,7 +3284,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3212
3284
|
{
|
|
3213
3285
|
name: "interrupt",
|
|
3214
3286
|
aliases: ["int"],
|
|
3215
|
-
usage: "/interrupt
|
|
3287
|
+
usage: "/interrupt [teammate] [message]",
|
|
3216
3288
|
description: "Interrupt a running agent and resume with a steering message",
|
|
3217
3289
|
run: (args) => this.cmdInterrupt(args),
|
|
3218
3290
|
},
|
|
@@ -3265,6 +3337,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3265
3337
|
description: "Ask a quick side question without interrupting the main conversation",
|
|
3266
3338
|
run: (args) => this.cmdBtw(args),
|
|
3267
3339
|
},
|
|
3340
|
+
{
|
|
3341
|
+
name: "script",
|
|
3342
|
+
aliases: [],
|
|
3343
|
+
usage: "/script [description]",
|
|
3344
|
+
description: "Write and run reusable scripts via the coding agent",
|
|
3345
|
+
run: (args) => this.cmdScript(args),
|
|
3346
|
+
},
|
|
3268
3347
|
{
|
|
3269
3348
|
name: "theme",
|
|
3270
3349
|
aliases: [],
|
|
@@ -3536,7 +3615,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3536
3615
|
this.refreshView();
|
|
3537
3616
|
}
|
|
3538
3617
|
/**
|
|
3539
|
-
* /interrupt
|
|
3618
|
+
* /interrupt [teammate] [message] — Kill a running agent and resume with context.
|
|
3540
3619
|
*/
|
|
3541
3620
|
async cmdInterrupt(argsStr) {
|
|
3542
3621
|
const parts = argsStr.trim().split(/\s+/);
|
|
@@ -3544,7 +3623,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3544
3623
|
const steeringMessage = parts.slice(1).join(" ").trim() ||
|
|
3545
3624
|
"Wrap up your current work and report what you've done so far.";
|
|
3546
3625
|
if (!teammateName) {
|
|
3547
|
-
this.feedLine(tp.warning(" Usage: /interrupt
|
|
3626
|
+
this.feedLine(tp.warning(" Usage: /interrupt [teammate] [message]"));
|
|
3548
3627
|
this.refreshView();
|
|
3549
3628
|
return;
|
|
3550
3629
|
}
|
|
@@ -3656,9 +3735,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3656
3735
|
try {
|
|
3657
3736
|
{
|
|
3658
3737
|
// btw and debug tasks skip conversation context (not part of main thread)
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3738
|
+
const isMainThread = entry.type !== "btw" && entry.type !== "debug";
|
|
3739
|
+
// Snapshot-aware context building: if the entry has a frozen snapshot
|
|
3740
|
+
// (@everyone), use it directly — no mutation of shared state.
|
|
3741
|
+
// Otherwise, compress live state as before.
|
|
3742
|
+
const snapshot = entry.type === "agent" ? entry.contextSnapshot : undefined;
|
|
3743
|
+
if (isMainThread && !snapshot)
|
|
3744
|
+
this.preDispatchCompress();
|
|
3745
|
+
const extraContext = isMainThread
|
|
3746
|
+
? this.buildConversationContext(entry.teammate, snapshot)
|
|
3747
|
+
: "";
|
|
3662
3748
|
let result = await this.orchestrator.assign({
|
|
3663
3749
|
teammate: entry.teammate,
|
|
3664
3750
|
task: entry.task,
|
|
@@ -4307,6 +4393,14 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4307
4393
|
}
|
|
4308
4394
|
}
|
|
4309
4395
|
this.pendingMigrationSyncs = migrationCount;
|
|
4396
|
+
// If no migration tasks were actually queued, commit version now
|
|
4397
|
+
if (migrationCount === 0) {
|
|
4398
|
+
this.commitVersionUpdate();
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
else if (versionUpdate) {
|
|
4402
|
+
// No migration needed — commit the version update immediately
|
|
4403
|
+
this.commitVersionUpdate();
|
|
4310
4404
|
}
|
|
4311
4405
|
this.kickDrain();
|
|
4312
4406
|
// 3. Purge daily logs older than 30 days (disk + Vectra)
|
|
@@ -4334,8 +4428,8 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4334
4428
|
}
|
|
4335
4429
|
/**
|
|
4336
4430
|
* Check if the CLI version has changed since last run.
|
|
4337
|
-
*
|
|
4338
|
-
*
|
|
4431
|
+
* Does NOT update settings.json — call `commitVersionUpdate()` after
|
|
4432
|
+
* migration tasks are complete to persist the new version.
|
|
4339
4433
|
*/
|
|
4340
4434
|
checkVersionUpdate() {
|
|
4341
4435
|
const settingsPath = join(this.teammatesDir, "settings.json");
|
|
@@ -4344,17 +4438,29 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4344
4438
|
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4345
4439
|
}
|
|
4346
4440
|
catch {
|
|
4347
|
-
// No settings file or invalid JSON
|
|
4441
|
+
// No settings file or invalid JSON
|
|
4348
4442
|
}
|
|
4349
4443
|
const previous = settings.cliVersion ?? "";
|
|
4350
4444
|
const current = PKG_VERSION;
|
|
4351
4445
|
if (previous === current)
|
|
4352
4446
|
return null;
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4447
|
+
return { previous, current };
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Persist the current CLI version to settings.json.
|
|
4451
|
+
* Called after all migration tasks complete (or immediately if no migration needed).
|
|
4452
|
+
*/
|
|
4453
|
+
commitVersionUpdate() {
|
|
4454
|
+
const settingsPath = join(this.teammatesDir, "settings.json");
|
|
4455
|
+
let settings = {};
|
|
4456
|
+
try {
|
|
4457
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4458
|
+
}
|
|
4459
|
+
catch {
|
|
4460
|
+
// No settings file or invalid JSON — create one
|
|
4461
|
+
}
|
|
4462
|
+
const previous = settings.cliVersion ?? "";
|
|
4463
|
+
const current = PKG_VERSION;
|
|
4358
4464
|
settings.cliVersion = current;
|
|
4359
4465
|
if (!settings.version)
|
|
4360
4466
|
settings.version = 1;
|
|
@@ -4364,12 +4470,15 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4364
4470
|
catch {
|
|
4365
4471
|
/* write failed — non-fatal */
|
|
4366
4472
|
}
|
|
4473
|
+
// Detect major/minor version change (not just patch)
|
|
4474
|
+
const [prevMajor, prevMinor] = previous.split(".").map(Number);
|
|
4475
|
+
const [curMajor, curMinor] = current.split(".").map(Number);
|
|
4476
|
+
const isMajorMinor = previous !== "" && (prevMajor !== curMajor || prevMinor !== curMinor);
|
|
4367
4477
|
if (isMajorMinor) {
|
|
4368
4478
|
this.feedLine(tp.accent(` ✔ Updated from v${previous} → v${current}`));
|
|
4369
4479
|
this.feedLine();
|
|
4370
4480
|
this.refreshView();
|
|
4371
4481
|
}
|
|
4372
|
-
return { previous, current };
|
|
4373
4482
|
}
|
|
4374
4483
|
async cmdCopy() {
|
|
4375
4484
|
this.doCopy(); // copies entire session
|
|
@@ -4524,6 +4633,116 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4524
4633
|
this.refreshView();
|
|
4525
4634
|
this.kickDrain();
|
|
4526
4635
|
}
|
|
4636
|
+
async cmdScript(argsStr) {
|
|
4637
|
+
const args = argsStr.trim();
|
|
4638
|
+
const scriptsDir = join(this.teammatesDir, this.selfName, "scripts");
|
|
4639
|
+
// /script (no args) — show usage
|
|
4640
|
+
if (!args) {
|
|
4641
|
+
this.feedLine();
|
|
4642
|
+
this.feedLine(tp.bold(" /script — write and run reusable scripts"));
|
|
4643
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
4644
|
+
this.feedLine(concat(tp.accent(" /script list".padEnd(36)), tp.text("List saved scripts")));
|
|
4645
|
+
this.feedLine(concat(tp.accent(" /script run <name>".padEnd(36)), tp.text("Run an existing script")));
|
|
4646
|
+
this.feedLine(concat(tp.accent(" /script <description>".padEnd(36)), tp.text("Create and run a new script")));
|
|
4647
|
+
this.feedLine();
|
|
4648
|
+
this.feedLine(tp.muted(` Scripts are saved to ${scriptsDir}`));
|
|
4649
|
+
this.feedLine();
|
|
4650
|
+
this.refreshView();
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
// /script list — list saved scripts
|
|
4654
|
+
if (args === "list") {
|
|
4655
|
+
let files = [];
|
|
4656
|
+
try {
|
|
4657
|
+
files = readdirSync(scriptsDir).filter((f) => !f.startsWith("."));
|
|
4658
|
+
}
|
|
4659
|
+
catch {
|
|
4660
|
+
// directory doesn't exist yet
|
|
4661
|
+
}
|
|
4662
|
+
this.feedLine();
|
|
4663
|
+
if (files.length === 0) {
|
|
4664
|
+
this.feedLine(tp.muted(" No scripts saved yet."));
|
|
4665
|
+
this.feedLine(tp.muted(" Use /script <description> to create one."));
|
|
4666
|
+
}
|
|
4667
|
+
else {
|
|
4668
|
+
this.feedLine(tp.bold(" Saved scripts"));
|
|
4669
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
4670
|
+
for (const f of files) {
|
|
4671
|
+
this.feedLine(concat(tp.accent(` ${f}`)));
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
this.feedLine();
|
|
4675
|
+
this.refreshView();
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
// /script run <name> — run an existing script
|
|
4679
|
+
if (args.startsWith("run ")) {
|
|
4680
|
+
const name = args.slice(4).trim();
|
|
4681
|
+
if (!name) {
|
|
4682
|
+
this.feedLine(tp.muted(" Usage: /script run <name>"));
|
|
4683
|
+
this.refreshView();
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
// Find the script file (try exact match, then with common extensions)
|
|
4687
|
+
const candidates = [
|
|
4688
|
+
name,
|
|
4689
|
+
`${name}.sh`,
|
|
4690
|
+
`${name}.ts`,
|
|
4691
|
+
`${name}.js`,
|
|
4692
|
+
`${name}.ps1`,
|
|
4693
|
+
`${name}.py`,
|
|
4694
|
+
];
|
|
4695
|
+
let scriptPath = null;
|
|
4696
|
+
for (const c of candidates) {
|
|
4697
|
+
const p = join(scriptsDir, c);
|
|
4698
|
+
if (existsSync(p)) {
|
|
4699
|
+
scriptPath = p;
|
|
4700
|
+
break;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
if (!scriptPath) {
|
|
4704
|
+
this.feedLine(tp.warning(` Script not found: ${name}`));
|
|
4705
|
+
this.feedLine(tp.muted(" Use /script list to see available scripts."));
|
|
4706
|
+
this.refreshView();
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4709
|
+
const scriptContent = readFileSync(scriptPath, "utf-8");
|
|
4710
|
+
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.`;
|
|
4711
|
+
this.taskQueue.push({
|
|
4712
|
+
type: "script",
|
|
4713
|
+
teammate: this.selfName,
|
|
4714
|
+
task,
|
|
4715
|
+
});
|
|
4716
|
+
this.feedLine(concat(tp.muted(" Running script "), tp.accent(basename(scriptPath)), tp.muted(" → "), tp.accent(`@${this.adapterName}`)));
|
|
4717
|
+
this.feedLine();
|
|
4718
|
+
this.refreshView();
|
|
4719
|
+
this.kickDrain();
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
// /script <description> — create and run a new script
|
|
4723
|
+
const task = [
|
|
4724
|
+
`The user wants a reusable script. Their request:`,
|
|
4725
|
+
``,
|
|
4726
|
+
args,
|
|
4727
|
+
``,
|
|
4728
|
+
`Instructions:`,
|
|
4729
|
+
`1. Write the script and save it to the scripts directory: ${scriptsDir}`,
|
|
4730
|
+
`2. Create the directory if it doesn't exist.`,
|
|
4731
|
+
`3. Choose a short, descriptive filename (kebab-case, with appropriate extension like .sh, .ts, .js, .py, .ps1).`,
|
|
4732
|
+
`4. Make the script executable if applicable.`,
|
|
4733
|
+
`5. Run the script and report the results.`,
|
|
4734
|
+
`6. If the script needs to be parameterized, use command-line arguments.`,
|
|
4735
|
+
].join("\n");
|
|
4736
|
+
this.taskQueue.push({
|
|
4737
|
+
type: "script",
|
|
4738
|
+
teammate: this.selfName,
|
|
4739
|
+
task,
|
|
4740
|
+
});
|
|
4741
|
+
this.feedLine(concat(tp.muted(" Script task → "), tp.accent(`@${this.adapterName}`)));
|
|
4742
|
+
this.feedLine();
|
|
4743
|
+
this.refreshView();
|
|
4744
|
+
this.kickDrain();
|
|
4745
|
+
}
|
|
4527
4746
|
async cmdTheme() {
|
|
4528
4747
|
const t = theme();
|
|
4529
4748
|
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/onboard.js
CHANGED
|
@@ -108,6 +108,19 @@ async function isTeammateFolder(dirPath) {
|
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Detect whether a teammate folder is a human avatar (**Type:** human in SOUL.md).
|
|
113
|
+
* Human avatars are user-specific and should not be imported to other projects.
|
|
114
|
+
*/
|
|
115
|
+
async function isHumanAvatar(dirPath) {
|
|
116
|
+
try {
|
|
117
|
+
const soul = await readFile(join(dirPath, "SOUL.md"), "utf-8");
|
|
118
|
+
return /\*\*Type:\*\*\s*human/i.test(soul);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
111
124
|
/**
|
|
112
125
|
* Import teammates from another project's .teammates/ dir.
|
|
113
126
|
*
|
|
@@ -138,6 +151,9 @@ export async function importTeammates(sourceDir, targetDir) {
|
|
|
138
151
|
if (entry.isDirectory() && !isNonTeammateEntry(entry.name)) {
|
|
139
152
|
// Check if it's a teammate folder
|
|
140
153
|
if (await isTeammateFolder(srcPath)) {
|
|
154
|
+
// Skip human avatar folders — those are user-specific, not team members
|
|
155
|
+
if (await isHumanAvatar(srcPath))
|
|
156
|
+
continue;
|
|
141
157
|
// Skip if teammate already exists in target
|
|
142
158
|
try {
|
|
143
159
|
await stat(destPath);
|
|
@@ -165,18 +181,8 @@ export async function importTeammates(sourceDir, targetDir) {
|
|
|
165
181
|
teammates.push(entry.name);
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// PROTOCOL.md, TEMPLATE.md) are project-specific and get created fresh
|
|
171
|
-
// from the template by copyTemplateFiles().
|
|
172
|
-
try {
|
|
173
|
-
await stat(destPath);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
await copyFile(srcPath, destPath);
|
|
177
|
-
files.push(entry.name);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
184
|
+
// USER.md is user-specific (gitignored) — never import it.
|
|
185
|
+
// The new user creates their own profile during setup.
|
|
180
186
|
}
|
|
181
187
|
// Ensure .gitignore exists
|
|
182
188
|
const gitignoreDest = join(targetDir, ".gitignore");
|
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