@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 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.`, " - 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 (\`name\`, \`description\`, \`type\`). 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.");
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.");
@@ -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)
@@ -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 replyId = `reply-${teammate}-${Date.now()}`;
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: "copy",
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
- /** Token budget for recent conversation history (24k tokens 96k chars). */
157
- static CONV_HISTORY_CHARS = 24_000 * 4;
158
- /** Threshold (chars) above which conversation history is written to a temp file. */
159
- static CONV_FILE_THRESHOLD = 8_000;
160
- buildConversationContext() {
161
- const context = buildConvCtx(this.conversationHistory, this.conversationSummary, TeammatesREPL.CONV_HISTORY_CHARS);
162
- // If the conversation context is very long, write it to a temp file
163
- // and return a pointer instead — reduces inline prompt noise.
164
- if (context.length > TeammatesREPL.CONV_FILE_THRESHOLD) {
165
- const tmpDir = join(this.teammatesDir, ".tmp");
166
- mkdirSync(tmpDir, { recursive: true });
167
- const convFile = join(tmpDir, "conversation.md");
168
- writeFileSync(convFile, context, "utf-8");
169
- return `## Conversation History\n\nThe full conversation history is in \`.teammates/.tmp/conversation.md\`. Read it before responding to understand prior context.`;
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 24k token budget.
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 === "copy") {
2707
- this.doCopy(this.lastCleanedOutput || undefined);
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 <teammate> [message]",
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 <teammate> [message] — Kill a running agent and resume with context.
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 <teammate> [message]"));
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 extraContext = entry.type === "btw" || entry.type === "debug"
3660
- ? ""
3661
- : this.buildConversationContext();
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
- * Updates settings.json with the current version.
4338
- * Returns the previous version if it changed, null otherwise.
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 — create one
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
- // Detect major/minor version change (not just patch)
4354
- const [prevMajor, prevMinor] = previous.split(".").map(Number);
4355
- const [curMajor, curMinor] = current.split(".").map(Number);
4356
- const isMajorMinor = previous !== "" && (prevMajor !== curMajor || prevMinor !== curMinor);
4357
- // Update the stored version
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 content = await readFile(join(memoryDir, entry), "utf-8");
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 content = await readFile(join(weeklyDir, entry), "utf-8");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammates/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Agent-agnostic CLI for teammates. Routes tasks, manages handoffs, and plugs into any coding agent backend.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",