@teammates/cli 0.5.0 → 0.5.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/cli-args.js CHANGED
@@ -97,28 +97,28 @@ export async function resolveAdapter(name, opts = {}) {
97
97
  }
98
98
  // ─── Usage ───────────────────────────────────────────────────────────
99
99
  export function printUsage() {
100
- console.log(`
101
- ${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
102
-
103
- ${chalk.bold("Usage:")}
104
- teammates <agent> Launch session with an agent
105
- teammates codex Use OpenAI Codex
106
- teammates aider Use Aider
107
-
108
- ${chalk.bold("Options:")}
109
- --model <model> Override the agent model
110
- --dir <path> Override .teammates/ location
111
-
112
- ${chalk.bold("Agents:")}
113
- claude Claude Code CLI (requires 'claude' on PATH)
114
- codex OpenAI Codex CLI (requires 'codex' on PATH)
115
- aider Aider CLI (requires 'aider' on PATH)
116
- echo Test adapter — echoes prompts (no external agent)
117
-
118
- ${chalk.bold("In-session:")}
119
- @teammate <task> Assign directly via @mention
120
- <text> Auto-route to the best teammate
121
- /status Session overview
122
- /help All commands
100
+ console.log(`
101
+ ${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
102
+
103
+ ${chalk.bold("Usage:")}
104
+ teammates <agent> Launch session with an agent
105
+ teammates codex Use OpenAI Codex
106
+ teammates aider Use Aider
107
+
108
+ ${chalk.bold("Options:")}
109
+ --model <model> Override the agent model
110
+ --dir <path> Override .teammates/ location
111
+
112
+ ${chalk.bold("Agents:")}
113
+ claude Claude Code CLI (requires 'claude' on PATH)
114
+ codex OpenAI Codex CLI (requires 'codex' on PATH)
115
+ aider Aider CLI (requires 'aider' on PATH)
116
+ echo Test adapter — echoes prompts (no external agent)
117
+
118
+ ${chalk.bold("In-session:")}
119
+ @teammate <task> Assign directly via @mention
120
+ <text> Auto-route to the best teammate
121
+ /status Session overview
122
+ /help All commands
123
123
  `.trim());
124
124
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, } from "./cli-args.js";
6
+ describe("parseCliArgs", () => {
7
+ it("returns defaults with no arguments", () => {
8
+ const result = parseCliArgs([]);
9
+ expect(result).toEqual({
10
+ showHelp: false,
11
+ modelOverride: undefined,
12
+ dirOverride: undefined,
13
+ adapterName: "echo",
14
+ agentPassthrough: [],
15
+ });
16
+ });
17
+ it("parses --help flag", () => {
18
+ const result = parseCliArgs(["--help"]);
19
+ expect(result.showHelp).toBe(true);
20
+ });
21
+ it("parses --model option with value", () => {
22
+ const result = parseCliArgs(["--model", "gpt-4"]);
23
+ expect(result.modelOverride).toBe("gpt-4");
24
+ });
25
+ it("parses --dir option with value", () => {
26
+ const result = parseCliArgs(["--dir", "/custom/path"]);
27
+ expect(result.dirOverride).toBe("/custom/path");
28
+ });
29
+ it("extracts adapter name as first positional argument", () => {
30
+ const result = parseCliArgs(["claude"]);
31
+ expect(result.adapterName).toBe("claude");
32
+ });
33
+ it("passes remaining args as agentPassthrough", () => {
34
+ const result = parseCliArgs(["claude", "--verbose", "--debug"]);
35
+ expect(result.adapterName).toBe("claude");
36
+ expect(result.agentPassthrough).toEqual(["--verbose", "--debug"]);
37
+ });
38
+ it("handles all options together", () => {
39
+ const result = parseCliArgs([
40
+ "--help",
41
+ "--model",
42
+ "sonnet",
43
+ "--dir",
44
+ "/tmp/tm",
45
+ "codex",
46
+ "--extra",
47
+ ]);
48
+ expect(result.showHelp).toBe(true);
49
+ expect(result.modelOverride).toBe("sonnet");
50
+ expect(result.dirOverride).toBe("/tmp/tm");
51
+ expect(result.adapterName).toBe("codex");
52
+ expect(result.agentPassthrough).toEqual(["--extra"]);
53
+ });
54
+ it("does not consume --model when no value follows", () => {
55
+ const result = parseCliArgs(["--model"]);
56
+ // getOption finds --model but no value after it, so it's left in args
57
+ // args.shift() then picks it up as the adapter name
58
+ expect(result.modelOverride).toBeUndefined();
59
+ expect(result.adapterName).toBe("--model");
60
+ });
61
+ it("does not treat --help value as an option value", () => {
62
+ const result = parseCliArgs(["--help", "claude"]);
63
+ expect(result.showHelp).toBe(true);
64
+ expect(result.adapterName).toBe("claude");
65
+ });
66
+ });
67
+ // ── PKG_VERSION ────────────────────────────────────────────────────
68
+ describe("PKG_VERSION", () => {
69
+ it("is a valid semver-like string", () => {
70
+ expect(PKG_VERSION).toMatch(/^\d+\.\d+\.\d+/);
71
+ });
72
+ });
73
+ // ── findTeammatesDir ───────────────────────────────────────────────
74
+ describe("findTeammatesDir", () => {
75
+ let testDir;
76
+ beforeEach(async () => {
77
+ testDir = join(tmpdir(), `cli-args-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
78
+ await mkdir(testDir, { recursive: true });
79
+ });
80
+ afterEach(async () => {
81
+ await rm(testDir, { recursive: true, force: true });
82
+ });
83
+ it("returns dirOverride when provided", async () => {
84
+ const result = await findTeammatesDir("/some/custom/dir");
85
+ expect(result).toContain("some");
86
+ });
87
+ it("returns .teammates dir when it exists under cwd", async () => {
88
+ const tmDir = join(testDir, ".teammates");
89
+ await mkdir(tmDir, { recursive: true });
90
+ // Mock process.cwd to return our test dir
91
+ const origCwd = process.cwd;
92
+ process.cwd = () => testDir;
93
+ try {
94
+ const result = await findTeammatesDir(undefined);
95
+ expect(result).toBe(tmDir);
96
+ }
97
+ finally {
98
+ process.cwd = origCwd;
99
+ }
100
+ });
101
+ it("returns null when no .teammates dir exists", async () => {
102
+ const origCwd = process.cwd;
103
+ process.cwd = () => testDir;
104
+ try {
105
+ const result = await findTeammatesDir(undefined);
106
+ expect(result).toBeNull();
107
+ }
108
+ finally {
109
+ process.cwd = origCwd;
110
+ }
111
+ });
112
+ });
113
+ // ── printUsage ────────────────────────────────────────────────────
114
+ describe("printUsage", () => {
115
+ it("prints usage text to console", () => {
116
+ const spy = vi.spyOn(console, "log").mockImplementation(() => { });
117
+ printUsage();
118
+ expect(spy).toHaveBeenCalled();
119
+ const output = spy.mock.calls[0][0];
120
+ expect(output).toContain("teammates");
121
+ expect(output).toContain("--model");
122
+ expect(output).toContain("--dir");
123
+ spy.mockRestore();
124
+ });
125
+ });
package/dist/cli.js CHANGED
@@ -15,16 +15,16 @@ 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) {
@@ -233,7 +231,6 @@ class TeammatesREPL {
233
231
  ctrlcPending = false; // true after first Ctrl+C, waiting for second
234
232
  ctrlcTimer = null;
235
233
  lastCleanedOutput = ""; // last teammate output for clipboard copy
236
- dispatching = false;
237
234
  autoApproveHandoffs = false;
238
235
  /** Last debug log file path per teammate — for /debug analysis. */
239
236
  lastDebugFiles = new Map();
@@ -948,14 +945,14 @@ class TeammatesREPL {
948
945
  const changes = proposals
949
946
  .map((p) => `- **Proposal ${p.index}: ${p.title}**\n - Section: ${p.section}\n - Before: ${p.before}\n - After: ${p.after}`)
950
947
  .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
-
948
+ const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
949
+
950
+ **Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
951
+
952
+ ${changes}
953
+
954
+ After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
955
+
959
956
  Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily log.`;
960
957
  this.taskQueue.push({ type: "agent", teammate, task: applyPrompt });
961
958
  this.feedLine(concat(tp.muted(" Queued SOUL.md update for "), tp.accent(`@${teammate}`)));
@@ -997,7 +994,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
997
994
  mentioned = [];
998
995
  while ((m = mentionRegex.exec(input)) !== null) {
999
996
  // Remap adapter name alias → user avatar for routing
1000
- const name = (m[1] === this.adapterName && this.userAlias) ? this.selfName : m[1];
997
+ const name = m[1] === this.adapterName && this.userAlias ? this.selfName : m[1];
1001
998
  if (allNames.includes(name) && !mentioned.includes(name)) {
1002
999
  mentioned.push(name);
1003
1000
  }
@@ -1168,11 +1165,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1168
1165
  const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
1169
1166
  await scaffoldFromPersona(teammatesDir, folderName, p);
1170
1167
  created.push(folderName);
1171
- console.log(chalk.green(" ✔ ") + chalk.white(`@${folderName}`) + chalk.gray(` — ${p.persona}`));
1168
+ console.log(chalk.green(" ✔ ") +
1169
+ chalk.white(`@${folderName}`) +
1170
+ chalk.gray(` — ${p.persona}`));
1172
1171
  }
1173
1172
  console.log();
1174
- console.log(chalk.green(` ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `) +
1175
- chalk.white(created.map((n) => `@${n}`).join(", ")));
1173
+ console.log(chalk.green(` ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `) + chalk.white(created.map((n) => `@${n}`).join(", ")));
1176
1174
  console.log(chalk.gray(" Tip: Your agent will adapt ownership and capabilities to this codebase on first task."));
1177
1175
  console.log();
1178
1176
  }
@@ -1658,8 +1656,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1658
1656
  };
1659
1657
  this.writeUserProfile(teammatesDir, login, answers);
1660
1658
  this.createUserAvatar(teammatesDir, login, answers);
1661
- console.log(chalk.green(" ✔ ") +
1662
- chalk.gray(`Profile created — avatar @${login}`));
1659
+ console.log(chalk.green(" ✔ ") + chalk.gray(`Profile created — avatar @${login}`));
1663
1660
  console.log();
1664
1661
  }
1665
1662
  /**
@@ -1669,7 +1666,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1669
1666
  console.log();
1670
1667
  console.log(chalk.gray(" (alias is required, press Enter to skip others)\n"));
1671
1668
  const aliasRaw = await this.askInput("Your alias (e.g., alex): ");
1672
- const alias = aliasRaw.toLowerCase().replace(/[^a-z0-9_-]/g, "").trim();
1669
+ const alias = aliasRaw
1670
+ .toLowerCase()
1671
+ .replace(/[^a-z0-9_-]/g, "")
1672
+ .trim();
1673
1673
  if (!alias) {
1674
1674
  console.log(chalk.yellow(" Alias is required. Run /user to try again.\n"));
1675
1675
  return;
@@ -1692,8 +1692,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1692
1692
  this.writeUserProfile(teammatesDir, alias, answers);
1693
1693
  this.createUserAvatar(teammatesDir, alias, answers);
1694
1694
  console.log();
1695
- console.log(chalk.green(" ✔ ") +
1696
- chalk.gray(`Profile created — avatar @${alias}`));
1695
+ console.log(chalk.green(" ✔ ") + chalk.gray(`Profile created — avatar @${alias}`));
1697
1696
  console.log(chalk.gray(" Update anytime with /user"));
1698
1697
  console.log();
1699
1698
  }
@@ -1781,12 +1780,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1781
1780
  if (roleMatch)
1782
1781
  role = roleMatch[1].trim();
1783
1782
  }
1784
- catch { /* avatar folder may not exist yet */ }
1783
+ catch {
1784
+ /* avatar folder may not exist yet */
1785
+ }
1785
1786
  let wisdom = "";
1786
1787
  try {
1787
1788
  wisdom = readFileSync(join(avatarDir, "WISDOM.md"), "utf-8");
1788
1789
  }
1789
- catch { /* ok */ }
1790
+ catch {
1791
+ /* ok */
1792
+ }
1790
1793
  registry.register({
1791
1794
  name: alias,
1792
1795
  type: "human",
@@ -1799,7 +1802,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1799
1802
  routingKeywords: [],
1800
1803
  });
1801
1804
  // Set presence to online (local user is always online)
1802
- this.orchestrator.getAllStatuses().set(alias, { state: "idle", presence: "online" });
1805
+ this.orchestrator
1806
+ .getAllStatuses()
1807
+ .set(alias, { state: "idle", presence: "online" });
1803
1808
  // Update the adapter name so tasks route to the avatar
1804
1809
  this.userAlias = alias;
1805
1810
  }
@@ -1891,9 +1896,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1891
1896
  if (completedArgs > 0)
1892
1897
  return [];
1893
1898
  const lower = partial.toLowerCase();
1894
- return TeammatesREPL.CONFIGURABLE_SERVICES
1895
- .filter((s) => s.startsWith(lower))
1896
- .map((s) => ({
1899
+ return TeammatesREPL.CONFIGURABLE_SERVICES.filter((s) => s.startsWith(lower)).map((s) => ({
1897
1900
  label: s,
1898
1901
  description: `configure ${s}`,
1899
1902
  completion: `/${cmdName} ${s} `,
@@ -2184,7 +2187,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2184
2187
  routingKeywords: [],
2185
2188
  cwd: dirname(this.teammatesDir),
2186
2189
  });
2187
- this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle", presence: "online" });
2190
+ this.orchestrator
2191
+ .getAllStatuses()
2192
+ .set(this.adapterName, { state: "idle", presence: "online" });
2188
2193
  }
2189
2194
  // Populate roster on the adapter so prompts include team info
2190
2195
  // Exclude the user avatar and adapter fallback — neither is an addressable teammate
@@ -2210,7 +2215,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2210
2215
  borderStyle: (s) => chalk.gray(s),
2211
2216
  colorize: (value) => {
2212
2217
  const validNames = new Set([
2213
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
2218
+ ...this.orchestrator
2219
+ .listTeammates()
2220
+ .filter((n) => n !== this.adapterName && n !== this.userAlias),
2214
2221
  this.adapterName,
2215
2222
  "everyone",
2216
2223
  ]);
@@ -2258,9 +2265,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2258
2265
  const bannerTeammates = [];
2259
2266
  // Add user avatar first (displayed as adapter name alias)
2260
2267
  if (this.userAlias) {
2261
- const ut = reg.get(this.userAlias);
2262
2268
  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 });
2269
+ bannerTeammates.push({
2270
+ name: this.adapterName,
2271
+ role: "Coding agent that performs tasks on your behalf.",
2272
+ presence: up,
2273
+ });
2264
2274
  }
2265
2275
  for (const name of names) {
2266
2276
  const t = reg.get(name);
@@ -2299,7 +2309,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2299
2309
  }
2300
2310
  // Colorize @mentions only if they reference a valid teammate or the user
2301
2311
  const validNames = new Set([
2302
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
2312
+ ...this.orchestrator
2313
+ .listTeammates()
2314
+ .filter((n) => n !== this.adapterName && n !== this.userAlias),
2303
2315
  this.adapterName,
2304
2316
  "everyone",
2305
2317
  ]);
@@ -2591,7 +2603,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2591
2603
  const preMentions = [];
2592
2604
  while ((pm = preMentionRegex.exec(rawLine)) !== null) {
2593
2605
  // Remap adapter name alias → user avatar for routing
2594
- const name = (pm[1] === this.adapterName && this.userAlias) ? this.selfName : pm[1];
2606
+ const name = pm[1] === this.adapterName && this.userAlias ? this.selfName : pm[1];
2595
2607
  if (allNames.includes(name) && !preMentions.includes(name)) {
2596
2608
  preMentions.push(name);
2597
2609
  }
@@ -2663,16 +2675,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2663
2675
  }
2664
2676
  // Slash commands
2665
2677
  if (input.startsWith("/")) {
2666
- this.dispatching = true;
2667
2678
  try {
2668
2679
  await this.dispatch(input);
2669
2680
  }
2670
2681
  catch (err) {
2671
2682
  this.feedLine(tp.error(`Error: ${err.message}`));
2672
2683
  }
2673
- finally {
2674
- this.dispatching = false;
2675
- }
2676
2684
  this.refreshView();
2677
2685
  return;
2678
2686
  }
@@ -2710,14 +2718,22 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2710
2718
  // Show user avatar first (displayed as adapter name alias)
2711
2719
  if (this.userAlias) {
2712
2720
  const up = statuses.get(this.userAlias)?.presence ?? "online";
2713
- const udot = up === "online" ? tp.success("●") : up === "reachable" ? tp.warning("●") : tp.error("●");
2721
+ const udot = up === "online"
2722
+ ? tp.success("●")
2723
+ : up === "reachable"
2724
+ ? tp.warning("●")
2725
+ : tp.error("●");
2714
2726
  this.feedLine(concat(tp.text(" "), udot, tp.accent(` @${this.adapterName.padEnd(14)}`), tp.muted("Coding agent that performs tasks on your behalf.")));
2715
2727
  }
2716
2728
  for (const name of teammates) {
2717
2729
  const t = registry.get(name);
2718
2730
  if (t) {
2719
2731
  const p = statuses.get(name)?.presence ?? "online";
2720
- const dot = p === "online" ? tp.success("●") : p === "reachable" ? tp.warning("●") : tp.error("●");
2732
+ const dot = p === "online"
2733
+ ? tp.success("●")
2734
+ : p === "reachable"
2735
+ ? tp.warning("●")
2736
+ : tp.error("●");
2721
2737
  this.feedLine(concat(tp.text(" "), dot, tp.accent(` @${name.padEnd(14)}`), tp.muted(t.role)));
2722
2738
  }
2723
2739
  }
@@ -2910,7 +2926,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2910
2926
  // Get username for confirmation
2911
2927
  let username = "";
2912
2928
  try {
2913
- username = execSync("gh api user --jq .login", { stdio: "pipe", encoding: "utf-8" }).trim();
2929
+ username = execSync("gh api user --jq .login", {
2930
+ stdio: "pipe",
2931
+ encoding: "utf-8",
2932
+ }).trim();
2914
2933
  }
2915
2934
  catch {
2916
2935
  // non-critical
@@ -3090,7 +3109,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3090
3109
  }
3091
3110
  break;
3092
3111
  }
3093
- case "error":
3112
+ case "error": {
3094
3113
  this.activeTasks.delete(event.teammate);
3095
3114
  if (this.activeTasks.size === 0)
3096
3115
  this.stopStatusAnimation();
@@ -3100,6 +3119,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3100
3119
  this.feedLine(tp.error(` ✖ ${displayErr}: ${event.error}`));
3101
3120
  this.showPrompt();
3102
3121
  break;
3122
+ }
3103
3123
  }
3104
3124
  }
3105
3125
  async cmdStatus() {
@@ -3628,8 +3648,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3628
3648
  // Start draining
3629
3649
  this.kickDrain();
3630
3650
  }
3631
- /** Run compaction + recall index update for a single teammate. */
3632
- async runCompact(name) {
3651
+ /**
3652
+ * Run compaction + recall index update for a single teammate.
3653
+ * When `silent` is true, routine status messages go to the progress bar
3654
+ * only — the feed is reserved for actual work (weeklies/monthlies created).
3655
+ */
3656
+ async runCompact(name, silent = false) {
3633
3657
  const teammateDir = join(this.teammatesDir, name);
3634
3658
  if (this.chatView) {
3635
3659
  this.chatView.setProgress(`Compacting ${name}...`);
@@ -3640,8 +3664,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3640
3664
  spinner = ora({ text: `Compacting ${name}...`, color: "cyan" }).start();
3641
3665
  }
3642
3666
  try {
3667
+ // Auto-compact daily logs if they exceed the token budget (creates partial weeklies)
3668
+ const autoResult = await autoCompactForBudget(teammateDir, DAILY_LOG_BUDGET_TOKENS);
3669
+ // Regular episodic compaction (complete weeks → weeklies, old weeklies → monthlies)
3643
3670
  const result = await compactEpisodic(teammateDir, name);
3644
3671
  const parts = [];
3672
+ if (autoResult) {
3673
+ parts.push(`${autoResult.created.length} auto-compacted (budget overflow)`);
3674
+ }
3645
3675
  if (result.weekliesCreated.length > 0) {
3646
3676
  parts.push(`${result.weekliesCreated.length} weekly summaries created`);
3647
3677
  }
@@ -3657,10 +3687,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3657
3687
  if (parts.length === 0) {
3658
3688
  if (spinner)
3659
3689
  spinner.info(`${name}: nothing to compact`);
3660
- if (this.chatView)
3690
+ // Silent: progress bar only; verbose: feed line
3691
+ if (this.chatView && !silent)
3661
3692
  this.feedLine(tp.muted(` ℹ ${name}: nothing to compact`));
3662
3693
  }
3663
3694
  else {
3695
+ // Actual work done — always show in feed
3664
3696
  if (spinner)
3665
3697
  spinner.succeed(`${name}: ${parts.join(", ")}`);
3666
3698
  if (this.chatView)
@@ -3686,7 +3718,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3686
3718
  syncSpinner.succeed(`${name}: index synced`);
3687
3719
  if (this.chatView) {
3688
3720
  this.chatView.setProgress(null);
3689
- this.feedLine(tp.success(` ✔ ${name}: index synced`));
3721
+ if (!silent)
3722
+ this.feedLine(tp.success(` ✔ ${name}: index synced`));
3690
3723
  }
3691
3724
  }
3692
3725
  catch {
@@ -3702,7 +3735,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3702
3735
  teammate: name,
3703
3736
  task: wisdomPrompt,
3704
3737
  });
3705
- if (this.chatView)
3738
+ if (this.chatView && !silent)
3706
3739
  this.feedLine(tp.muted(` ↻ ${name}: queued wisdom distillation`));
3707
3740
  }
3708
3741
  }
@@ -3716,6 +3749,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3716
3749
  spinner.fail(`${name}: ${msg}`);
3717
3750
  if (this.chatView) {
3718
3751
  this.chatView.setProgress(null);
3752
+ // Errors always show in feed
3719
3753
  this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3720
3754
  }
3721
3755
  }
@@ -3750,34 +3784,34 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3750
3784
  this.refreshView();
3751
3785
  return;
3752
3786
  }
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.
3787
+ 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.
3788
+
3789
+ Produce a response with these four sections:
3790
+
3791
+ ## 1. What's Working
3792
+ 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.
3793
+
3794
+ ## 2. What's Not Working
3795
+ Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
3796
+
3797
+ ## 3. Proposed SOUL.md Changes
3798
+ The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
3799
+
3800
+ **Proposal N: <short title>**
3801
+ - **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
3802
+ - **Before:** <the current text to replace, or "(new entry)" if adding>
3803
+ - **After:** <the exact replacement text>
3804
+ - **Why:** <evidence from recent work justifying the change>
3805
+
3806
+ Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
3807
+
3808
+ ## 4. Questions for the Team
3809
+ Issues that can't be resolved unilaterally — they need input from other teammates or the user.
3810
+
3811
+ **Rules:**
3812
+ - This is a self-review of YOUR work. Do not evaluate other teammates.
3813
+ - Evidence over opinion — cite specific examples.
3814
+ - No busywork — if everything is working well, say "all good, no changes." That's a valid outcome.
3781
3815
  - Number each proposal (Proposal 1, Proposal 2, etc.) so the user can approve or reject individually.`;
3782
3816
  const label = targets.length > 1
3783
3817
  ? targets.map((n) => `@${n}`).join(", ")
@@ -3842,36 +3876,35 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3842
3876
  .filter((n) => n !== this.selfName && n !== this.adapterName);
3843
3877
  if (teammates.length === 0)
3844
3878
  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 = [];
3879
+ // 1. Run compaction for all teammates (auto-compact + episodic + sync + wisdom)
3880
+ // Progress bar shows status; feed only shows lines when actual work is done
3850
3881
  for (const name of teammates) {
3851
- const memoryDir = join(this.teammatesDir, name, "memory");
3852
- 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);
3862
- }
3863
- catch {
3864
- /* no memory dir */
3882
+ if (this.chatView) {
3883
+ this.chatView.setProgress(`Maintaining @${name}...`);
3884
+ this.refreshView();
3865
3885
  }
3886
+ await this.runCompact(name, true);
3866
3887
  }
3867
- if (needsCompact.length > 0) {
3868
- this.feedLine(concat(tp.muted(" Compacting stale logs for "), tp.accent(needsCompact.map((n) => `@${n}`).join(", ")), tp.muted("...")));
3888
+ if (this.chatView) {
3889
+ this.chatView.setProgress(null);
3869
3890
  this.refreshView();
3870
- for (const name of needsCompact) {
3871
- await this.runCompact(name);
3891
+ }
3892
+ // 2. Purge daily logs older than 30 days (disk + Vectra)
3893
+ const { Indexer } = await import("@teammates/recall");
3894
+ const indexer = new Indexer({ teammatesDir: this.teammatesDir });
3895
+ for (const name of teammates) {
3896
+ try {
3897
+ const purged = await purgeStaleDailies(join(this.teammatesDir, name));
3898
+ for (const file of purged) {
3899
+ const uri = `${name}/memory/${file}`;
3900
+ await indexer.deleteDocument(name, uri).catch(() => { });
3901
+ }
3902
+ }
3903
+ catch {
3904
+ /* purge failed — non-fatal */
3872
3905
  }
3873
3906
  }
3874
- // 2. Sync recall indexes (bundled library call)
3907
+ // 3. Sync recall indexes (bundled library call)
3875
3908
  try {
3876
3909
  await syncRecallIndex(this.teammatesDir);
3877
3910
  }