@teammates/cli 0.5.0 → 0.5.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/cli.js CHANGED
@@ -10,21 +10,21 @@
10
10
  import { exec as execCb, execSync, spawnSync } from "node:child_process";
11
11
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
13
- import { dirname, join, resolve } from "node:path";
13
+ import { 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";
17
17
  import ora from "ora";
18
- import { syncRecallIndex } from "./adapter.js";
19
- import { AnimatedBanner } from "./banner.js";
18
+ import { DAILY_LOG_BUDGET_TOKENS, syncRecallIndex } from "./adapter.js";
19
+ import { AnimatedBanner, } from "./banner.js";
20
20
  import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, resolveAdapter, } from "./cli-args.js";
21
21
  import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
22
- import { buildWisdomPrompt, compactEpisodic } from "./compact.js";
22
+ import { autoCompactForBudget, buildWisdomPrompt, compactEpisodic, purgeStaleDailies, } from "./compact.js";
23
23
  import { PromptInput } from "./console/prompt-input.js";
24
24
  import { buildTitle } from "./console/startup.js";
25
25
  import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
26
- import { loadPersonas, scaffoldFromPersona } from "./personas.js";
27
26
  import { Orchestrator } from "./orchestrator.js";
27
+ import { loadPersonas, scaffoldFromPersona } from "./personas.js";
28
28
  import { colorToHex, theme, tp } from "./theme.js";
29
29
  // ─── Parsed CLI arguments ────────────────────────────────────────────
30
30
  const cliArgs = parseCliArgs();
@@ -69,9 +69,7 @@ class TeammatesREPL {
69
69
  .trim();
70
70
  // Header: "teammate: subject"
71
71
  const subject = result.summary || "Task completed";
72
- const displayTeammate = result.teammate === this.selfName
73
- ? this.adapterName
74
- : result.teammate;
72
+ const displayTeammate = result.teammate === this.selfName ? this.adapterName : result.teammate;
75
73
  this.feedLine(concat(tp.accent(`${displayTeammate}: `), tp.text(subject)));
76
74
  this.lastCleanedOutput = cleaned;
77
75
  if (cleaned) {
@@ -219,6 +217,8 @@ class TeammatesREPL {
219
217
  taskQueue = [];
220
218
  /** Per-agent active tasks — one per agent running in parallel. */
221
219
  agentActive = new Map();
220
+ /** Active system tasks — multiple can run concurrently per agent. */
221
+ systemActive = new Map();
222
222
  /** Agents currently in a silent retry — suppress all events. */
223
223
  silentAgents = new Set();
224
224
  /** Per-agent drain locks — prevents double-draining a single agent. */
@@ -233,7 +233,6 @@ class TeammatesREPL {
233
233
  ctrlcPending = false; // true after first Ctrl+C, waiting for second
234
234
  ctrlcTimer = null;
235
235
  lastCleanedOutput = ""; // last teammate output for clipboard copy
236
- dispatching = false;
237
236
  autoApproveHandoffs = false;
238
237
  /** Last debug log file path per teammate — for /debug analysis. */
239
238
  lastDebugFiles = new Map();
@@ -333,37 +332,74 @@ class TeammatesREPL {
333
332
  this.input.setStatus(null);
334
333
  }
335
334
  }
335
+ /**
336
+ * Truncate a path for display, collapsing middle segments if too long.
337
+ * E.g. C:\source\some\deep\project → C:\source\...\project
338
+ */
339
+ static truncatePath(fullPath, maxLen = 30) {
340
+ if (fullPath.length <= maxLen)
341
+ return fullPath;
342
+ const parts = fullPath.split(sep);
343
+ if (parts.length <= 2)
344
+ return fullPath;
345
+ const last = parts[parts.length - 1];
346
+ // Keep adding segments from the front until we'd exceed maxLen
347
+ let front = parts[0];
348
+ for (let i = 1; i < parts.length - 1; i++) {
349
+ const candidate = front + sep + parts[i] + sep + "..." + sep + last;
350
+ if (candidate.length > maxLen)
351
+ break;
352
+ front += sep + parts[i];
353
+ }
354
+ return front + sep + "..." + sep + last;
355
+ }
356
+ /** Format elapsed seconds as (Ns), (Nm Ns), or (Nh Nm Ns). */
357
+ static formatElapsed(totalSeconds) {
358
+ const s = totalSeconds % 60;
359
+ const m = Math.floor(totalSeconds / 60) % 60;
360
+ const h = Math.floor(totalSeconds / 3600);
361
+ if (h > 0)
362
+ return `(${h}h ${m}m ${s}s)`;
363
+ if (m > 0)
364
+ return `(${m}m ${s}s)`;
365
+ return `(${s}s)`;
366
+ }
336
367
  /** Render one frame of the status animation. */
337
368
  renderStatusFrame() {
338
369
  if (this.activeTasks.size === 0)
339
370
  return;
340
371
  const entries = Array.from(this.activeTasks.values());
341
- const idx = this.statusRotateIndex % entries.length;
342
- const { teammate, task } = entries[idx];
372
+ const total = entries.length;
373
+ const idx = this.statusRotateIndex % total;
374
+ const { teammate, task, startTime } = entries[idx];
343
375
  const displayName = teammate === this.selfName ? this.adapterName : teammate;
344
376
  const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
345
- const taskPreview = task.length > 50 ? `${task.slice(0, 47)}...` : task;
346
- const queueInfo = this.activeTasks.size > 1 ? ` (${idx + 1}/${this.activeTasks.size})` : "";
347
- if (this.chatView) {
348
- // Strip newlines and truncate task text for single-line display
349
- const cleanTask = task.replace(/[\r\n]+/g, " ").trim();
350
- const maxLen = Math.max(20, (process.stdout.columns || 80) - displayName.length - 10);
351
- const taskText = cleanTask.length > maxLen
352
- ? `${cleanTask.slice(0, maxLen - 1)}…`
377
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
378
+ const elapsedStr = TeammatesREPL.formatElapsed(elapsed);
379
+ // Build the tag: (1/3 - 2m 5s) when multiple, (2m 5s) when single
380
+ const tag = total > 1
381
+ ? `(${idx + 1}/${total} - ${elapsedStr.slice(1, -1)})`
382
+ : elapsedStr;
383
+ // Target 80 chars total: "<spinner> <name>... <task> <tag>"
384
+ const prefix = `${spinChar} ${displayName}... `;
385
+ const suffix = ` ${tag}`;
386
+ const maxTask = 80 - prefix.length - suffix.length;
387
+ const cleanTask = task.replace(/[\r\n]+/g, " ").trim();
388
+ const taskText = maxTask <= 3
389
+ ? ""
390
+ : cleanTask.length > maxTask
391
+ ? `${cleanTask.slice(0, maxTask - 1)}…`
353
392
  : cleanTask;
354
- const queueTag = this.activeTasks.size > 1
355
- ? ` (${idx + 1}/${this.activeTasks.size})`
356
- : "";
357
- this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}… `), tp.muted(taskText + queueTag)));
393
+ if (this.chatView) {
394
+ this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}... `), tp.muted(`${taskText}${suffix}`)));
358
395
  this.app.refresh();
359
396
  }
360
397
  else {
361
- // Mostly bright blue, periodically flicker to dark blue
362
398
  const spinColor = this.statusFrame % 8 === 0 ? chalk.blue : chalk.blueBright;
363
399
  const line = ` ${spinColor(spinChar)} ` +
364
400
  chalk.bold(displayName) +
365
- chalk.gray(`… ${taskPreview}`) +
366
- (queueInfo ? chalk.gray(queueInfo) : "");
401
+ chalk.gray(`... ${taskText}`) +
402
+ chalk.gray(suffix);
367
403
  this.input.setStatus(line);
368
404
  }
369
405
  }
@@ -948,14 +984,14 @@ class TeammatesREPL {
948
984
  const changes = proposals
949
985
  .map((p) => `- **Proposal ${p.index}: ${p.title}**\n - Section: ${p.section}\n - Before: ${p.before}\n - After: ${p.after}`)
950
986
  .join("\n\n");
951
- const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
952
-
953
- **Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
954
-
955
- ${changes}
956
-
957
- After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
958
-
987
+ const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
988
+
989
+ **Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
990
+
991
+ ${changes}
992
+
993
+ After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
994
+
959
995
  Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily log.`;
960
996
  this.taskQueue.push({ type: "agent", teammate, task: applyPrompt });
961
997
  this.feedLine(concat(tp.muted(" Queued SOUL.md update for "), tp.accent(`@${teammate}`)));
@@ -997,7 +1033,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
997
1033
  mentioned = [];
998
1034
  while ((m = mentionRegex.exec(input)) !== null) {
999
1035
  // Remap adapter name alias → user avatar for routing
1000
- const name = (m[1] === this.adapterName && this.userAlias) ? this.selfName : m[1];
1036
+ const name = m[1] === this.adapterName && this.userAlias ? this.selfName : m[1];
1001
1037
  if (allNames.includes(name) && !mentioned.includes(name)) {
1002
1038
  mentioned.push(name);
1003
1039
  }
@@ -1035,9 +1071,24 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1035
1071
  this.taskQueue.push({ type: "agent", teammate: match, task: input });
1036
1072
  this.kickDrain();
1037
1073
  }
1038
- /** Start draining per-agent queues in parallel. Each agent gets its own drain loop. */
1074
+ /** Returns true if the queue entry is a system-initiated (non-blocking) task. */
1075
+ isSystemTask(entry) {
1076
+ return (entry.type === "compact" ||
1077
+ entry.type === "summarize" ||
1078
+ (entry.type === "agent" && entry.system === true));
1079
+ }
1080
+ /** Start draining per-agent queues in parallel. Each agent gets its own drain loop.
1081
+ * System tasks are extracted and run concurrently without blocking user tasks. */
1039
1082
  kickDrain() {
1040
- // Find agents that have queued tasks but no active drain
1083
+ // Extract system tasks and fire them concurrently (non-blocking)
1084
+ for (let i = this.taskQueue.length - 1; i >= 0; i--) {
1085
+ const entry = this.taskQueue[i];
1086
+ if (this.isSystemTask(entry)) {
1087
+ this.taskQueue.splice(i, 1);
1088
+ this.runSystemTask(entry);
1089
+ }
1090
+ }
1091
+ // Find agents that have user tasks but no active drain
1041
1092
  const agentsWithWork = new Set();
1042
1093
  for (const entry of this.taskQueue) {
1043
1094
  agentsWithWork.add(entry.teammate);
@@ -1051,6 +1102,53 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1051
1102
  }
1052
1103
  }
1053
1104
  }
1105
+ /**
1106
+ * Run a system-initiated task concurrently without blocking user tasks.
1107
+ * Purely background — no progress bar, no /status. Only reports errors.
1108
+ */
1109
+ async runSystemTask(entry) {
1110
+ const taskId = `sys-${entry.teammate}-${Date.now()}`;
1111
+ this.systemActive.set(taskId, entry);
1112
+ const startTime = Date.now();
1113
+ try {
1114
+ if (entry.type === "compact") {
1115
+ await this.runCompact(entry.teammate, true);
1116
+ }
1117
+ else if (entry.type === "summarize") {
1118
+ const result = await this.orchestrator.assign({
1119
+ teammate: entry.teammate,
1120
+ task: entry.task,
1121
+ system: true,
1122
+ });
1123
+ const raw = result.rawOutput ?? "";
1124
+ this.conversationSummary = raw
1125
+ .replace(/^TO:\s*\S+\s*\n/im, "")
1126
+ .replace(/^#\s+.+\n*/m, "")
1127
+ .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
1128
+ .trim();
1129
+ }
1130
+ else {
1131
+ // System agent tasks (e.g. wisdom distillation)
1132
+ const result = await this.orchestrator.assign({
1133
+ teammate: entry.teammate,
1134
+ task: entry.task,
1135
+ system: true,
1136
+ });
1137
+ // Write debug entry for system tasks too
1138
+ this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
1139
+ }
1140
+ }
1141
+ catch (err) {
1142
+ // System task errors always show in feed
1143
+ const msg = err?.message ?? String(err);
1144
+ const displayName = entry.teammate === this.selfName ? this.adapterName : entry.teammate;
1145
+ this.feedLine(tp.error(` ✖ @${displayName} (system): ${msg}`));
1146
+ this.refreshView();
1147
+ }
1148
+ finally {
1149
+ this.systemActive.delete(taskId);
1150
+ }
1151
+ }
1054
1152
  // ─── Onboarding ───────────────────────────────────────────────────
1055
1153
  /**
1056
1154
  * Interactive prompt for team onboarding after user profile is set up.
@@ -1168,11 +1266,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1168
1266
  const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
1169
1267
  await scaffoldFromPersona(teammatesDir, folderName, p);
1170
1268
  created.push(folderName);
1171
- console.log(chalk.green(" ✔ ") + chalk.white(`@${folderName}`) + chalk.gray(` — ${p.persona}`));
1269
+ console.log(chalk.green(" ✔ ") +
1270
+ chalk.white(`@${folderName}`) +
1271
+ chalk.gray(` — ${p.persona}`));
1172
1272
  }
1173
1273
  console.log();
1174
- console.log(chalk.green(` ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `) +
1175
- chalk.white(created.map((n) => `@${n}`).join(", ")));
1274
+ console.log(chalk.green(` ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `) + chalk.white(created.map((n) => `@${n}`).join(", ")));
1176
1275
  console.log(chalk.gray(" Tip: Your agent will adapt ownership and capabilities to this codebase on first task."));
1177
1276
  console.log();
1178
1277
  }
@@ -1658,8 +1757,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1658
1757
  };
1659
1758
  this.writeUserProfile(teammatesDir, login, answers);
1660
1759
  this.createUserAvatar(teammatesDir, login, answers);
1661
- console.log(chalk.green(" ✔ ") +
1662
- chalk.gray(`Profile created — avatar @${login}`));
1760
+ console.log(chalk.green(" ✔ ") + chalk.gray(`Profile created — avatar @${login}`));
1663
1761
  console.log();
1664
1762
  }
1665
1763
  /**
@@ -1669,7 +1767,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1669
1767
  console.log();
1670
1768
  console.log(chalk.gray(" (alias is required, press Enter to skip others)\n"));
1671
1769
  const aliasRaw = await this.askInput("Your alias (e.g., alex): ");
1672
- const alias = aliasRaw.toLowerCase().replace(/[^a-z0-9_-]/g, "").trim();
1770
+ const alias = aliasRaw
1771
+ .toLowerCase()
1772
+ .replace(/[^a-z0-9_-]/g, "")
1773
+ .trim();
1673
1774
  if (!alias) {
1674
1775
  console.log(chalk.yellow(" Alias is required. Run /user to try again.\n"));
1675
1776
  return;
@@ -1692,8 +1793,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1692
1793
  this.writeUserProfile(teammatesDir, alias, answers);
1693
1794
  this.createUserAvatar(teammatesDir, alias, answers);
1694
1795
  console.log();
1695
- console.log(chalk.green(" ✔ ") +
1696
- chalk.gray(`Profile created — avatar @${alias}`));
1796
+ console.log(chalk.green(" ✔ ") + chalk.gray(`Profile created — avatar @${alias}`));
1697
1797
  console.log(chalk.gray(" Update anytime with /user"));
1698
1798
  console.log();
1699
1799
  }
@@ -1781,12 +1881,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1781
1881
  if (roleMatch)
1782
1882
  role = roleMatch[1].trim();
1783
1883
  }
1784
- catch { /* avatar folder may not exist yet */ }
1884
+ catch {
1885
+ /* avatar folder may not exist yet */
1886
+ }
1785
1887
  let wisdom = "";
1786
1888
  try {
1787
1889
  wisdom = readFileSync(join(avatarDir, "WISDOM.md"), "utf-8");
1788
1890
  }
1789
- catch { /* ok */ }
1891
+ catch {
1892
+ /* ok */
1893
+ }
1790
1894
  registry.register({
1791
1895
  name: alias,
1792
1896
  type: "human",
@@ -1799,7 +1903,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1799
1903
  routingKeywords: [],
1800
1904
  });
1801
1905
  // Set presence to online (local user is always online)
1802
- this.orchestrator.getAllStatuses().set(alias, { state: "idle", presence: "online" });
1906
+ this.orchestrator
1907
+ .getAllStatuses()
1908
+ .set(alias, { state: "idle", presence: "online" });
1803
1909
  // Update the adapter name so tasks route to the avatar
1804
1910
  this.userAlias = alias;
1805
1911
  }
@@ -1891,9 +1997,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1891
1997
  if (completedArgs > 0)
1892
1998
  return [];
1893
1999
  const lower = partial.toLowerCase();
1894
- return TeammatesREPL.CONFIGURABLE_SERVICES
1895
- .filter((s) => s.startsWith(lower))
1896
- .map((s) => ({
2000
+ return TeammatesREPL.CONFIGURABLE_SERVICES.filter((s) => s.startsWith(lower)).map((s) => ({
1897
2001
  label: s,
1898
2002
  description: `configure ${s}`,
1899
2003
  completion: `/${cmdName} ${s} `,
@@ -2184,7 +2288,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2184
2288
  routingKeywords: [],
2185
2289
  cwd: dirname(this.teammatesDir),
2186
2290
  });
2187
- this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle", presence: "online" });
2291
+ this.orchestrator
2292
+ .getAllStatuses()
2293
+ .set(this.adapterName, { state: "idle", presence: "online" });
2188
2294
  }
2189
2295
  // Populate roster on the adapter so prompts include team info
2190
2296
  // Exclude the user avatar and adapter fallback — neither is an addressable teammate
@@ -2210,7 +2316,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2210
2316
  borderStyle: (s) => chalk.gray(s),
2211
2317
  colorize: (value) => {
2212
2318
  const validNames = new Set([
2213
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
2319
+ ...this.orchestrator
2320
+ .listTeammates()
2321
+ .filter((n) => n !== this.adapterName && n !== this.userAlias),
2214
2322
  this.adapterName,
2215
2323
  "everyone",
2216
2324
  ]);
@@ -2258,9 +2366,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2258
2366
  const bannerTeammates = [];
2259
2367
  // Add user avatar first (displayed as adapter name alias)
2260
2368
  if (this.userAlias) {
2261
- const ut = reg.get(this.userAlias);
2262
2369
  const up = statuses.get(this.userAlias)?.presence ?? "online";
2263
- bannerTeammates.push({ name: this.adapterName, role: "Coding agent that performs tasks on your behalf.", presence: up });
2370
+ bannerTeammates.push({
2371
+ name: this.adapterName,
2372
+ role: "Coding agent that performs tasks on your behalf.",
2373
+ presence: up,
2374
+ });
2264
2375
  }
2265
2376
  for (const name of names) {
2266
2377
  const t = reg.get(name);
@@ -2299,7 +2410,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2299
2410
  }
2300
2411
  // Colorize @mentions only if they reference a valid teammate or the user
2301
2412
  const validNames = new Set([
2302
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
2413
+ ...this.orchestrator
2414
+ .listTeammates()
2415
+ .filter((n) => n !== this.adapterName && n !== this.userAlias),
2303
2416
  this.adapterName,
2304
2417
  "everyone",
2305
2418
  ]);
@@ -2343,11 +2456,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2343
2456
  progressStyle: { fg: t.progress, italic: true },
2344
2457
  dropdownHighlightStyle: { fg: t.accent },
2345
2458
  dropdownStyle: { fg: t.textMuted },
2346
- footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName)),
2459
+ footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName), tp.muted(" "), tp.dim(TeammatesREPL.truncatePath(dirname(this.teammatesDir)))),
2347
2460
  footerRight: tp.muted("? /help "),
2348
2461
  footerStyle: { fg: t.textDim },
2349
2462
  });
2350
- this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName));
2463
+ this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName), tp.muted(" "), tp.dim(TeammatesREPL.truncatePath(dirname(this.teammatesDir))));
2351
2464
  this.defaultFooterRight = tp.muted("? /help ");
2352
2465
  // Wire ChatView events for input handling
2353
2466
  this.chatView.on("submit", (rawLine) => {
@@ -2591,7 +2704,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2591
2704
  const preMentions = [];
2592
2705
  while ((pm = preMentionRegex.exec(rawLine)) !== null) {
2593
2706
  // Remap adapter name alias → user avatar for routing
2594
- const name = (pm[1] === this.adapterName && this.userAlias) ? this.selfName : pm[1];
2707
+ const name = pm[1] === this.adapterName && this.userAlias ? this.selfName : pm[1];
2595
2708
  if (allNames.includes(name) && !preMentions.includes(name)) {
2596
2709
  preMentions.push(name);
2597
2710
  }
@@ -2663,16 +2776,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2663
2776
  }
2664
2777
  // Slash commands
2665
2778
  if (input.startsWith("/")) {
2666
- this.dispatching = true;
2667
2779
  try {
2668
2780
  await this.dispatch(input);
2669
2781
  }
2670
2782
  catch (err) {
2671
2783
  this.feedLine(tp.error(`Error: ${err.message}`));
2672
2784
  }
2673
- finally {
2674
- this.dispatching = false;
2675
- }
2676
2785
  this.refreshView();
2677
2786
  return;
2678
2787
  }
@@ -2710,14 +2819,22 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2710
2819
  // Show user avatar first (displayed as adapter name alias)
2711
2820
  if (this.userAlias) {
2712
2821
  const up = statuses.get(this.userAlias)?.presence ?? "online";
2713
- const udot = up === "online" ? tp.success("●") : up === "reachable" ? tp.warning("●") : tp.error("●");
2822
+ const udot = up === "online"
2823
+ ? tp.success("●")
2824
+ : up === "reachable"
2825
+ ? tp.warning("●")
2826
+ : tp.error("●");
2714
2827
  this.feedLine(concat(tp.text(" "), udot, tp.accent(` @${this.adapterName.padEnd(14)}`), tp.muted("Coding agent that performs tasks on your behalf.")));
2715
2828
  }
2716
2829
  for (const name of teammates) {
2717
2830
  const t = registry.get(name);
2718
2831
  if (t) {
2719
2832
  const p = statuses.get(name)?.presence ?? "online";
2720
- const dot = p === "online" ? tp.success("●") : p === "reachable" ? tp.warning("●") : tp.error("●");
2833
+ const dot = p === "online"
2834
+ ? tp.success("●")
2835
+ : p === "reachable"
2836
+ ? tp.warning("●")
2837
+ : tp.error("●");
2721
2838
  this.feedLine(concat(tp.text(" "), dot, tp.accent(` @${name.padEnd(14)}`), tp.muted(t.role)));
2722
2839
  }
2723
2840
  }
@@ -2910,7 +3027,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2910
3027
  // Get username for confirmation
2911
3028
  let username = "";
2912
3029
  try {
2913
- username = execSync("gh api user --jq .login", { stdio: "pipe", encoding: "utf-8" }).trim();
3030
+ username = execSync("gh api user --jq .login", {
3031
+ stdio: "pipe",
3032
+ encoding: "utf-8",
3033
+ }).trim();
2914
3034
  }
2915
3035
  catch {
2916
3036
  // non-critical
@@ -2949,7 +3069,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2949
3069
  {
2950
3070
  name: "debug",
2951
3071
  aliases: ["raw"],
2952
- usage: "/debug [teammate]",
3072
+ usage: "/debug [teammate] [focus]",
2953
3073
  description: "Analyze the last agent task with the coding agent",
2954
3074
  run: (args) => this.cmdDebug(args),
2955
3075
  },
@@ -3070,16 +3190,24 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3070
3190
  return;
3071
3191
  switch (event.type) {
3072
3192
  case "task_assigned": {
3193
+ // System tasks (compaction, summarization, wisdom distillation) are
3194
+ // invisible — don't track them in the progress bar.
3195
+ if (event.assignment.system)
3196
+ break;
3073
3197
  // Track this task and start the animated status bar
3074
3198
  const key = event.assignment.teammate;
3075
3199
  this.activeTasks.set(key, {
3076
3200
  teammate: event.assignment.teammate,
3077
3201
  task: event.assignment.task,
3202
+ startTime: Date.now(),
3078
3203
  });
3079
3204
  this.startStatusAnimation();
3080
3205
  break;
3081
3206
  }
3082
3207
  case "task_completed": {
3208
+ // System task completions — don't touch activeTasks (was never added)
3209
+ if (event.result.system)
3210
+ break;
3083
3211
  // Remove from active tasks and stop spinner.
3084
3212
  // Result display is deferred to drainAgentQueue() so the defensive
3085
3213
  // retry can update rawOutput before anything is shown to the user.
@@ -3090,7 +3218,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3090
3218
  }
3091
3219
  break;
3092
3220
  }
3093
- case "error":
3221
+ case "error": {
3094
3222
  this.activeTasks.delete(event.teammate);
3095
3223
  if (this.activeTasks.size === 0)
3096
3224
  this.stopStatusAnimation();
@@ -3100,6 +3228,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3100
3228
  this.feedLine(tp.error(` ✖ ${displayErr}: ${event.error}`));
3101
3229
  this.showPrompt();
3102
3230
  break;
3231
+ }
3103
3232
  }
3104
3233
  }
3105
3234
  async cmdStatus() {
@@ -3166,10 +3295,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3166
3295
  this.refreshView();
3167
3296
  }
3168
3297
  async cmdDebug(argsStr) {
3169
- const arg = argsStr.trim().replace(/^@/, "");
3298
+ const parts = argsStr.trim().split(/\s+/);
3299
+ const firstArg = (parts[0] ?? "").replace(/^@/, "");
3300
+ // Everything after the teammate name is the debug focus
3301
+ const debugFocus = parts.slice(1).join(" ").trim() || undefined;
3170
3302
  // Resolve which teammate to debug
3171
3303
  let targetName;
3172
- if (arg === "everyone") {
3304
+ if (firstArg === "everyone") {
3173
3305
  // Pick all teammates with debug files, queue one analysis per teammate
3174
3306
  const names = [];
3175
3307
  for (const [name] of this.lastDebugFiles) {
@@ -3182,28 +3314,29 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3182
3314
  return;
3183
3315
  }
3184
3316
  for (const name of names) {
3185
- this.queueDebugAnalysis(name);
3317
+ this.queueDebugAnalysis(name, debugFocus);
3186
3318
  }
3187
3319
  return;
3188
3320
  }
3189
- else if (arg) {
3190
- targetName = arg;
3321
+ else if (firstArg) {
3322
+ targetName = firstArg;
3191
3323
  }
3192
3324
  else if (this.lastResult) {
3193
3325
  targetName = this.lastResult.teammate;
3194
3326
  }
3195
3327
  else {
3196
- this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate]"));
3328
+ this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate] [focus]"));
3197
3329
  this.refreshView();
3198
3330
  return;
3199
3331
  }
3200
- this.queueDebugAnalysis(targetName);
3332
+ this.queueDebugAnalysis(targetName, debugFocus);
3201
3333
  }
3202
3334
  /**
3203
3335
  * Queue a debug analysis task — sends the last request + debug log
3204
3336
  * to the base coding agent for analysis.
3337
+ * @param debugFocus Optional focus area the user wants to investigate
3205
3338
  */
3206
- queueDebugAnalysis(teammate) {
3339
+ queueDebugAnalysis(teammate, debugFocus) {
3207
3340
  const debugFile = this.lastDebugFiles.get(teammate);
3208
3341
  const lastPrompt = this.lastTaskPrompts.get(teammate);
3209
3342
  if (!debugFile) {
@@ -3221,8 +3354,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3221
3354
  this.refreshView();
3222
3355
  return;
3223
3356
  }
3357
+ const focusLine = debugFocus
3358
+ ? `\n\n**Focus your analysis on:** ${debugFocus}`
3359
+ : "";
3224
3360
  const analysisPrompt = [
3225
- `Analyze the following debug log from @${teammate}'s last task execution. Identify any issues, errors, or anomalies. If the response was empty, explain likely causes. Provide a concise diagnosis and suggest fixes if applicable.`,
3361
+ `Analyze the following debug log from @${teammate}'s last task execution. Identify any issues, errors, or anomalies. If the response was empty, explain likely causes. Provide a concise diagnosis and suggest fixes if applicable.${focusLine}`,
3226
3362
  "",
3227
3363
  "## Last Request Sent to Agent",
3228
3364
  "",
@@ -3234,6 +3370,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3234
3370
  ].join("\n");
3235
3371
  // Show the debug log path — ctrl+click to open
3236
3372
  this.feedLine(concat(tp.muted(" Debug log: "), tp.accent(debugFile)));
3373
+ if (debugFocus) {
3374
+ this.feedLine(tp.muted(` Focus: ${debugFocus}`));
3375
+ }
3237
3376
  this.feedLine(tp.muted(" Queuing analysis…"));
3238
3377
  this.refreshView();
3239
3378
  this.taskQueue.push({
@@ -3260,34 +3399,18 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3260
3399
  this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
3261
3400
  this.refreshView();
3262
3401
  }
3263
- /** Drain tasks for a single agent — runs in parallel with other agents. */
3402
+ /** Drain user tasks for a single agent — runs in parallel with other agents.
3403
+ * System tasks are handled separately by runSystemTask(). */
3264
3404
  async drainAgentQueue(agent) {
3265
3405
  while (true) {
3266
- const idx = this.taskQueue.findIndex((e) => e.teammate === agent);
3406
+ const idx = this.taskQueue.findIndex((e) => e.teammate === agent && !this.isSystemTask(e));
3267
3407
  if (idx < 0)
3268
3408
  break;
3269
3409
  const entry = this.taskQueue.splice(idx, 1)[0];
3270
3410
  this.agentActive.set(agent, entry);
3271
3411
  const startTime = Date.now();
3272
3412
  try {
3273
- if (entry.type === "compact") {
3274
- await this.runCompact(entry.teammate);
3275
- }
3276
- else if (entry.type === "summarize") {
3277
- // Internal housekeeping — summarize older conversation history
3278
- const result = await this.orchestrator.assign({
3279
- teammate: entry.teammate,
3280
- task: entry.task,
3281
- });
3282
- // Extract the summary from the agent's output (strip protocol artifacts)
3283
- const raw = result.rawOutput ?? "";
3284
- this.conversationSummary = raw
3285
- .replace(/^TO:\s*\S+\s*\n/im, "")
3286
- .replace(/^#\s+.+\n*/m, "")
3287
- .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
3288
- .trim();
3289
- }
3290
- else {
3413
+ {
3291
3414
  // btw and debug tasks skip conversation context (not part of main thread)
3292
3415
  const extraContext = entry.type === "btw" || entry.type === "debug"
3293
3416
  ? ""
@@ -3398,6 +3521,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3398
3521
  task,
3399
3522
  "",
3400
3523
  ];
3524
+ // Include the full prompt sent to the agent (with identity, memory, etc.)
3525
+ const fullPrompt = result?.fullPrompt;
3526
+ if (fullPrompt) {
3527
+ lines.push("## Full Prompt");
3528
+ lines.push("");
3529
+ lines.push(fullPrompt);
3530
+ lines.push("");
3531
+ }
3401
3532
  if (error) {
3402
3533
  lines.push("## Result");
3403
3534
  lines.push("");
@@ -3454,7 +3585,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3454
3585
  lines.push("");
3455
3586
  writeFileSync(debugFile, lines.join("\n"), "utf-8");
3456
3587
  this.lastDebugFiles.set(teammate, debugFile);
3457
- this.lastTaskPrompts.set(teammate, task);
3588
+ this.lastTaskPrompts.set(teammate, fullPrompt ?? task);
3458
3589
  }
3459
3590
  catch {
3460
3591
  // Don't let debug logging break task execution
@@ -3628,20 +3759,30 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3628
3759
  // Start draining
3629
3760
  this.kickDrain();
3630
3761
  }
3631
- /** Run compaction + recall index update for a single teammate. */
3632
- async runCompact(name) {
3762
+ /**
3763
+ * Run compaction + recall index update for a single teammate.
3764
+ * When `silent` is true, routine status messages go to the progress bar
3765
+ * only — the feed is reserved for actual work (weeklies/monthlies created).
3766
+ */
3767
+ async runCompact(name, silent = false) {
3633
3768
  const teammateDir = join(this.teammatesDir, name);
3634
- if (this.chatView) {
3769
+ if (!silent && this.chatView) {
3635
3770
  this.chatView.setProgress(`Compacting ${name}...`);
3636
3771
  this.refreshView();
3637
3772
  }
3638
3773
  let spinner = null;
3639
- if (!this.chatView) {
3774
+ if (!silent && !this.chatView) {
3640
3775
  spinner = ora({ text: `Compacting ${name}...`, color: "cyan" }).start();
3641
3776
  }
3642
3777
  try {
3778
+ // Auto-compact daily logs if they exceed the token budget (creates partial weeklies)
3779
+ const autoResult = await autoCompactForBudget(teammateDir, DAILY_LOG_BUDGET_TOKENS);
3780
+ // Regular episodic compaction (complete weeks → weeklies, old weeklies → monthlies)
3643
3781
  const result = await compactEpisodic(teammateDir, name);
3644
3782
  const parts = [];
3783
+ if (autoResult) {
3784
+ parts.push(`${autoResult.created.length} auto-compacted (budget overflow)`);
3785
+ }
3645
3786
  if (result.weekliesCreated.length > 0) {
3646
3787
  parts.push(`${result.weekliesCreated.length} weekly summaries created`);
3647
3788
  }
@@ -3657,25 +3798,27 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3657
3798
  if (parts.length === 0) {
3658
3799
  if (spinner)
3659
3800
  spinner.info(`${name}: nothing to compact`);
3660
- if (this.chatView)
3801
+ // Silent: progress bar only; verbose: feed line
3802
+ if (this.chatView && !silent)
3661
3803
  this.feedLine(tp.muted(` ℹ ${name}: nothing to compact`));
3662
3804
  }
3663
3805
  else {
3806
+ // Actual work done — always show in feed
3664
3807
  if (spinner)
3665
3808
  spinner.succeed(`${name}: ${parts.join(", ")}`);
3666
3809
  if (this.chatView)
3667
3810
  this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
3668
3811
  }
3669
- if (this.chatView)
3812
+ if (!silent && this.chatView)
3670
3813
  this.chatView.setProgress(null);
3671
3814
  // Sync recall index for this teammate (bundled library call)
3672
3815
  try {
3673
- if (this.chatView) {
3816
+ if (!silent && this.chatView) {
3674
3817
  this.chatView.setProgress(`Syncing ${name} index...`);
3675
3818
  this.refreshView();
3676
3819
  }
3677
3820
  let syncSpinner = null;
3678
- if (!this.chatView) {
3821
+ if (!silent && !this.chatView) {
3679
3822
  syncSpinner = ora({
3680
3823
  text: `Syncing ${name} index...`,
3681
3824
  color: "cyan",
@@ -3685,8 +3828,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3685
3828
  if (syncSpinner)
3686
3829
  syncSpinner.succeed(`${name}: index synced`);
3687
3830
  if (this.chatView) {
3688
- this.chatView.setProgress(null);
3689
- this.feedLine(tp.success(` ✔ ${name}: index synced`));
3831
+ if (!silent)
3832
+ this.chatView.setProgress(null);
3833
+ if (!silent)
3834
+ this.feedLine(tp.success(` ✔ ${name}: index synced`));
3690
3835
  }
3691
3836
  }
3692
3837
  catch {
@@ -3701,9 +3846,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3701
3846
  type: "agent",
3702
3847
  teammate: name,
3703
3848
  task: wisdomPrompt,
3849
+ system: true,
3704
3850
  });
3705
- if (this.chatView)
3706
- this.feedLine(tp.muted(` ↻ ${name}: queued wisdom distillation`));
3851
+ this.kickDrain();
3707
3852
  }
3708
3853
  }
3709
3854
  catch {
@@ -3715,7 +3860,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3715
3860
  if (spinner)
3716
3861
  spinner.fail(`${name}: ${msg}`);
3717
3862
  if (this.chatView) {
3718
- this.chatView.setProgress(null);
3863
+ if (!silent)
3864
+ this.chatView.setProgress(null);
3865
+ // Errors always show in feed
3719
3866
  this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3720
3867
  }
3721
3868
  }
@@ -3750,34 +3897,34 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3750
3897
  this.refreshView();
3751
3898
  return;
3752
3899
  }
3753
- const retroPrompt = `Run a structured self-retrospective. Review your SOUL.md, WISDOM.md, your last 2-3 weekly summaries (or last 7 daily logs if no weeklies exist), and any typed memories in your memory/ folder.
3754
-
3755
- Produce a response with these four sections:
3756
-
3757
- ## 1. What's Working
3758
- Things you do well, based on evidence from recent work. Patterns worth reinforcing or codifying into wisdom. Cite specific examples from daily logs or memories.
3759
-
3760
- ## 2. What's Not Working
3761
- Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
3762
-
3763
- ## 3. Proposed SOUL.md Changes
3764
- The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
3765
-
3766
- **Proposal N: <short title>**
3767
- - **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
3768
- - **Before:** <the current text to replace, or "(new entry)" if adding>
3769
- - **After:** <the exact replacement text>
3770
- - **Why:** <evidence from recent work justifying the change>
3771
-
3772
- Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
3773
-
3774
- ## 4. Questions for the Team
3775
- Issues that can't be resolved unilaterally — they need input from other teammates or the user.
3776
-
3777
- **Rules:**
3778
- - This is a self-review of YOUR work. Do not evaluate other teammates.
3779
- - Evidence over opinion — cite specific examples.
3780
- - No busywork — if everything is working well, say "all good, no changes." That's a valid outcome.
3900
+ const retroPrompt = `Run a structured self-retrospective. Review your SOUL.md, WISDOM.md, your last 2-3 weekly summaries (or last 7 daily logs if no weeklies exist), and any typed memories in your memory/ folder.
3901
+
3902
+ Produce a response with these four sections:
3903
+
3904
+ ## 1. What's Working
3905
+ Things you do well, based on evidence from recent work. Patterns worth reinforcing or codifying into wisdom. Cite specific examples from daily logs or memories.
3906
+
3907
+ ## 2. What's Not Working
3908
+ Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
3909
+
3910
+ ## 3. Proposed SOUL.md Changes
3911
+ The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
3912
+
3913
+ **Proposal N: <short title>**
3914
+ - **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
3915
+ - **Before:** <the current text to replace, or "(new entry)" if adding>
3916
+ - **After:** <the exact replacement text>
3917
+ - **Why:** <evidence from recent work justifying the change>
3918
+
3919
+ Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
3920
+
3921
+ ## 4. Questions for the Team
3922
+ Issues that can't be resolved unilaterally — they need input from other teammates or the user.
3923
+
3924
+ **Rules:**
3925
+ - This is a self-review of YOUR work. Do not evaluate other teammates.
3926
+ - Evidence over opinion — cite specific examples.
3927
+ - No busywork — if everything is working well, say "all good, no changes." That's a valid outcome.
3781
3928
  - Number each proposal (Proposal 1, Proposal 2, etc.) so the user can approve or reject individually.`;
3782
3929
  const label = targets.length > 1
3783
3930
  ? targets.map((n) => `@${n}`).join(", ")
@@ -3842,36 +3989,27 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3842
3989
  .filter((n) => n !== this.selfName && n !== this.adapterName);
3843
3990
  if (teammates.length === 0)
3844
3991
  return;
3845
- // 1. Check each teammate for stale daily logs (older than 7 days)
3846
- const oneWeekAgo = new Date();
3847
- oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
3848
- const cutoff = oneWeekAgo.toISOString().slice(0, 10); // YYYY-MM-DD
3849
- const needsCompact = [];
3992
+ // 1. Run compaction for all teammates (auto-compact + episodic + sync + wisdom)
3993
+ // Progress bar shows status; feed only shows lines when actual work is done
3994
+ for (const name of teammates) {
3995
+ await this.runCompact(name, true);
3996
+ }
3997
+ // 2. Purge daily logs older than 30 days (disk + Vectra)
3998
+ const { Indexer } = await import("@teammates/recall");
3999
+ const indexer = new Indexer({ teammatesDir: this.teammatesDir });
3850
4000
  for (const name of teammates) {
3851
- const memoryDir = join(this.teammatesDir, name, "memory");
3852
4001
  try {
3853
- const entries = await readdir(memoryDir);
3854
- const hasStale = entries.some((e) => {
3855
- if (!e.endsWith(".md"))
3856
- return false;
3857
- const stem = e.replace(".md", "");
3858
- return /^\d{4}-\d{2}-\d{2}$/.test(stem) && stem < cutoff;
3859
- });
3860
- if (hasStale)
3861
- needsCompact.push(name);
4002
+ const purged = await purgeStaleDailies(join(this.teammatesDir, name));
4003
+ for (const file of purged) {
4004
+ const uri = `${name}/memory/${file}`;
4005
+ await indexer.deleteDocument(name, uri).catch(() => { });
4006
+ }
3862
4007
  }
3863
4008
  catch {
3864
- /* no memory dir */
3865
- }
3866
- }
3867
- if (needsCompact.length > 0) {
3868
- this.feedLine(concat(tp.muted(" Compacting stale logs for "), tp.accent(needsCompact.map((n) => `@${n}`).join(", ")), tp.muted("...")));
3869
- this.refreshView();
3870
- for (const name of needsCompact) {
3871
- await this.runCompact(name);
4009
+ /* purge failed non-fatal */
3872
4010
  }
3873
4011
  }
3874
- // 2. Sync recall indexes (bundled library call)
4012
+ // 3. Sync recall indexes (bundled library call)
3875
4013
  try {
3876
4014
  await syncRecallIndex(this.teammatesDir);
3877
4015
  }