@teammates/cli 0.4.1 → 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.
Files changed (47) hide show
  1. package/README.md +36 -4
  2. package/dist/adapter.d.ts +19 -3
  3. package/dist/adapter.js +168 -96
  4. package/dist/adapter.test.js +29 -16
  5. package/dist/adapters/cli-proxy.d.ts +3 -1
  6. package/dist/adapters/cli-proxy.js +65 -6
  7. package/dist/adapters/copilot.d.ts +3 -1
  8. package/dist/adapters/copilot.js +16 -3
  9. package/dist/adapters/echo.d.ts +3 -1
  10. package/dist/adapters/echo.js +4 -2
  11. package/dist/banner.js +5 -1
  12. package/dist/cli-args.js +23 -23
  13. package/dist/cli-args.test.d.ts +1 -0
  14. package/dist/cli-args.test.js +125 -0
  15. package/dist/cli.js +486 -220
  16. package/dist/compact.d.ts +23 -0
  17. package/dist/compact.js +181 -11
  18. package/dist/compact.test.js +323 -7
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +3 -1
  21. package/dist/onboard.js +165 -165
  22. package/dist/orchestrator.js +7 -2
  23. package/dist/personas.d.ts +42 -0
  24. package/dist/personas.js +108 -0
  25. package/dist/personas.test.d.ts +1 -0
  26. package/dist/personas.test.js +88 -0
  27. package/dist/registry.test.js +23 -23
  28. package/dist/theme.test.d.ts +1 -0
  29. package/dist/theme.test.js +113 -0
  30. package/dist/types.d.ts +2 -0
  31. package/package.json +4 -3
  32. package/personas/architect.md +95 -0
  33. package/personas/backend.md +97 -0
  34. package/personas/data-engineer.md +96 -0
  35. package/personas/designer.md +96 -0
  36. package/personas/devops.md +97 -0
  37. package/personas/frontend.md +98 -0
  38. package/personas/ml-ai.md +100 -0
  39. package/personas/mobile.md +97 -0
  40. package/personas/performance.md +96 -0
  41. package/personas/pm.md +93 -0
  42. package/personas/prompt-engineer.md +122 -0
  43. package/personas/qa.md +96 -0
  44. package/personas/security.md +96 -0
  45. package/personas/sre.md +97 -0
  46. package/personas/swe.md +92 -0
  47. package/personas/tech-writer.md +97 -0
package/dist/cli.js CHANGED
@@ -15,15 +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
26
  import { Orchestrator } from "./orchestrator.js";
27
+ import { loadPersonas, scaffoldFromPersona } from "./personas.js";
27
28
  import { colorToHex, theme, tp } from "./theme.js";
28
29
  // ─── Parsed CLI arguments ────────────────────────────────────────────
29
30
  const cliArgs = parseCliArgs();
@@ -48,6 +49,106 @@ class TeammatesREPL {
48
49
  text: result.summary,
49
50
  });
50
51
  }
52
+ /**
53
+ * Render a task result to the feed. Called from drainAgentQueue() AFTER
54
+ * the defensive retry so the user sees the final (possibly retried) output.
55
+ */
56
+ displayTaskResult(result, entryType) {
57
+ // Suppress display for internal summarization tasks
58
+ if (entryType === "summarize")
59
+ return;
60
+ if (!this.chatView)
61
+ this.input.deactivateAndErase();
62
+ const raw = result.rawOutput ?? "";
63
+ // Strip protocol artifacts
64
+ const cleaned = raw
65
+ .replace(/^TO:\s*\S+\s*\n/im, "")
66
+ .replace(/^#\s+.+\n*/m, "")
67
+ .replace(/```handoff\s*\n@\w+\s*\n[\s\S]*?```/g, "")
68
+ .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
69
+ .trim();
70
+ // Header: "teammate: subject"
71
+ const subject = result.summary || "Task completed";
72
+ const displayTeammate = result.teammate === this.selfName ? this.adapterName : result.teammate;
73
+ this.feedLine(concat(tp.accent(`${displayTeammate}: `), tp.text(subject)));
74
+ this.lastCleanedOutput = cleaned;
75
+ if (cleaned) {
76
+ this.feedMarkdown(cleaned);
77
+ }
78
+ else if (result.changedFiles.length > 0 || result.summary) {
79
+ // Agent produced no body text but DID do work — generate a synthetic
80
+ // summary from available metadata so the user sees something useful.
81
+ const syntheticLines = [];
82
+ if (result.summary) {
83
+ syntheticLines.push(result.summary);
84
+ }
85
+ if (result.changedFiles.length > 0) {
86
+ syntheticLines.push("");
87
+ syntheticLines.push("**Files changed:**");
88
+ for (const f of result.changedFiles) {
89
+ syntheticLines.push(`- ${f}`);
90
+ }
91
+ }
92
+ this.feedMarkdown(syntheticLines.join("\n"));
93
+ }
94
+ else {
95
+ this.feedLine(tp.muted(" (no response text — the agent may have only performed tool actions)"));
96
+ this.feedLine(tp.muted(` Use /debug ${result.teammate} to view full output`));
97
+ // Show diagnostic hints for empty responses
98
+ const diag = result.diagnostics;
99
+ if (diag) {
100
+ if (diag.exitCode !== 0 && diag.exitCode !== null) {
101
+ this.feedLine(tp.warning(` ⚠ Process exited with code ${diag.exitCode}`));
102
+ }
103
+ if (diag.signal) {
104
+ this.feedLine(tp.warning(` ⚠ Process killed by signal: ${diag.signal}`));
105
+ }
106
+ if (diag.debugFile) {
107
+ this.feedLine(tp.muted(` Debug log: ${diag.debugFile}`));
108
+ }
109
+ }
110
+ }
111
+ // Render handoffs
112
+ const handoffs = result.handoffs;
113
+ if (handoffs.length > 0) {
114
+ this.renderHandoffs(result.teammate, handoffs);
115
+ }
116
+ // Clickable [reply] [copy] actions after the response
117
+ if (this.chatView && cleaned) {
118
+ const t = theme();
119
+ const teammate = result.teammate;
120
+ const replyId = `reply-${teammate}-${Date.now()}`;
121
+ this._replyContexts.set(replyId, { teammate, message: cleaned });
122
+ this.chatView.appendActionList([
123
+ {
124
+ id: replyId,
125
+ normalStyle: this.makeSpan({
126
+ text: " [reply]",
127
+ style: { fg: t.textDim },
128
+ }),
129
+ hoverStyle: this.makeSpan({
130
+ text: " [reply]",
131
+ style: { fg: t.accent },
132
+ }),
133
+ },
134
+ {
135
+ id: "copy",
136
+ normalStyle: this.makeSpan({
137
+ text: " [copy]",
138
+ style: { fg: t.textDim },
139
+ }),
140
+ hoverStyle: this.makeSpan({
141
+ text: " [copy]",
142
+ style: { fg: t.accent },
143
+ }),
144
+ },
145
+ ]);
146
+ }
147
+ this.feedLine();
148
+ // Auto-detect new teammates added during this task
149
+ this.refreshTeammates();
150
+ this.showPrompt();
151
+ }
51
152
  /** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
52
153
  static CONV_HISTORY_CHARS = 24_000 * 4;
53
154
  buildConversationContext() {
@@ -116,6 +217,8 @@ class TeammatesREPL {
116
217
  taskQueue = [];
117
218
  /** Per-agent active tasks — one per agent running in parallel. */
118
219
  agentActive = new Map();
220
+ /** Agents currently in a silent retry — suppress all events. */
221
+ silentAgents = new Set();
119
222
  /** Per-agent drain locks — prevents double-draining a single agent. */
120
223
  agentDrainLocks = new Map();
121
224
  /** Stored pasted text keyed by paste number, expanded on Enter. */
@@ -128,7 +231,6 @@ class TeammatesREPL {
128
231
  ctrlcPending = false; // true after first Ctrl+C, waiting for second
129
232
  ctrlcTimer = null;
130
233
  lastCleanedOutput = ""; // last teammate output for clipboard copy
131
- dispatching = false;
132
234
  autoApproveHandoffs = false;
133
235
  /** Last debug log file path per teammate — for /debug analysis. */
134
236
  lastDebugFiles = new Map();
@@ -235,7 +337,7 @@ class TeammatesREPL {
235
337
  const entries = Array.from(this.activeTasks.values());
236
338
  const idx = this.statusRotateIndex % entries.length;
237
339
  const { teammate, task } = entries[idx];
238
- const displayName = teammate === this.adapterName ? this.selfName : teammate;
340
+ const displayName = teammate === this.selfName ? this.adapterName : teammate;
239
341
  const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
240
342
  const taskPreview = task.length > 50 ? `${task.slice(0, 47)}...` : task;
241
343
  const queueInfo = this.activeTasks.size > 1 ? ` (${idx + 1}/${this.activeTasks.size})` : "";
@@ -843,14 +945,14 @@ class TeammatesREPL {
843
945
  const changes = proposals
844
946
  .map((p) => `- **Proposal ${p.index}: ${p.title}**\n - Section: ${p.section}\n - Before: ${p.before}\n - After: ${p.after}`)
845
947
  .join("\n\n");
846
- const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
847
-
848
- **Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
849
-
850
- ${changes}
851
-
852
- After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
853
-
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
+
854
956
  Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily log.`;
855
957
  this.taskQueue.push({ type: "agent", teammate, task: applyPrompt });
856
958
  this.feedLine(concat(tp.muted(" Queued SOUL.md update for "), tp.accent(`@${teammate}`)));
@@ -891,7 +993,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
891
993
  let m;
892
994
  mentioned = [];
893
995
  while ((m = mentionRegex.exec(input)) !== null) {
894
- const name = m[1];
996
+ // Remap adapter name alias → user avatar for routing
997
+ const name = m[1] === this.adapterName && this.userAlias ? this.selfName : m[1];
895
998
  if (allNames.includes(name) && !mentioned.includes(name)) {
896
999
  mentioned.push(name);
897
1000
  }
@@ -921,7 +1024,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
921
1024
  {
922
1025
  const bg = this._userBg;
923
1026
  const t = theme();
924
- const displayName = match === this.adapterName ? this.selfName : match;
1027
+ const displayName = match === this.selfName ? this.adapterName : match;
925
1028
  this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${displayName}`)));
926
1029
  }
927
1030
  this.feedLine();
@@ -959,42 +1062,174 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
959
1062
  console.log(chalk.white(" Set up teammates for this project?\n"));
960
1063
  console.log(chalk.cyan(" 1") +
961
1064
  chalk.gray(") ") +
962
- chalk.white("New team") +
963
- chalk.gray(" — analyze this codebase and create teammates from scratch"));
1065
+ chalk.white("Pick teammates") +
1066
+ chalk.gray(" — choose from persona templates"));
964
1067
  console.log(chalk.cyan(" 2") +
1068
+ chalk.gray(") ") +
1069
+ chalk.white("Auto-generate") +
1070
+ chalk.gray(" — let your agent analyze the codebase and create teammates"));
1071
+ console.log(chalk.cyan(" 3") +
965
1072
  chalk.gray(") ") +
966
1073
  chalk.white("Import team") +
967
1074
  chalk.gray(" — copy teammates from another project"));
968
- console.log(chalk.cyan(" 3") +
1075
+ console.log(chalk.cyan(" 4") +
969
1076
  chalk.gray(") ") +
970
1077
  chalk.white("Solo mode") +
971
1078
  chalk.gray(" — use your agent without teammates"));
972
- console.log(chalk.cyan(" 4") + chalk.gray(") ") + chalk.white("Exit"));
1079
+ console.log(chalk.cyan(" 5") + chalk.gray(") ") + chalk.white("Exit"));
973
1080
  console.log();
974
- const choice = await this.askChoice("Pick an option (1/2/3/4): ", [
1081
+ const choice = await this.askChoice("Pick an option (1/2/3/4/5): ", [
975
1082
  "1",
976
1083
  "2",
977
1084
  "3",
978
1085
  "4",
1086
+ "5",
979
1087
  ]);
980
- if (choice === "4") {
1088
+ if (choice === "5") {
981
1089
  console.log(chalk.gray(" Goodbye."));
982
1090
  return false;
983
1091
  }
984
- if (choice === "3") {
1092
+ if (choice === "4") {
985
1093
  console.log(chalk.gray(" Running in solo mode — all tasks go to your agent."));
986
1094
  console.log(chalk.gray(" Run /init later to set up teammates."));
987
1095
  console.log();
988
1096
  return true;
989
1097
  }
990
- if (choice === "2") {
1098
+ if (choice === "3") {
991
1099
  await this.runImport(cwd);
992
1100
  return true;
993
1101
  }
994
- // choice === "1": Run onboarding via the agent
995
- await this.runOnboardingAgent(adapter, cwd);
1102
+ if (choice === "2") {
1103
+ // Auto-generate via agent
1104
+ await this.runOnboardingAgent(adapter, cwd);
1105
+ return true;
1106
+ }
1107
+ // choice === "1": Pick from persona templates
1108
+ await this.runPersonaOnboarding(teammatesDir);
996
1109
  return true;
997
1110
  }
1111
+ /**
1112
+ * Persona-based onboarding: show a list of bundled personas, let the user
1113
+ * pick which ones to create, optionally rename them, and scaffold the folders.
1114
+ */
1115
+ async runPersonaOnboarding(teammatesDir) {
1116
+ const personas = await loadPersonas();
1117
+ if (personas.length === 0) {
1118
+ console.log(chalk.yellow(" No persona templates found."));
1119
+ return;
1120
+ }
1121
+ console.log();
1122
+ console.log(chalk.white(" Available personas:\n"));
1123
+ // Display personas grouped by tier
1124
+ let currentTier = 0;
1125
+ for (let i = 0; i < personas.length; i++) {
1126
+ const p = personas[i];
1127
+ if (p.tier !== currentTier) {
1128
+ currentTier = p.tier;
1129
+ const label = currentTier === 1 ? "Core" : "Specialized";
1130
+ console.log(chalk.gray(` ── ${label} ──`));
1131
+ }
1132
+ const num = String(i + 1).padStart(2, " ");
1133
+ console.log(chalk.cyan(` ${num}`) +
1134
+ chalk.gray(") ") +
1135
+ chalk.white(p.persona) +
1136
+ chalk.gray(` (${p.alias})`) +
1137
+ chalk.gray(` — ${p.description}`));
1138
+ }
1139
+ console.log();
1140
+ console.log(chalk.gray(" Enter numbers separated by commas, e.g. 1,3,5"));
1141
+ console.log();
1142
+ const input = await this.askInput("Personas: ");
1143
+ if (!input) {
1144
+ console.log(chalk.gray(" No personas selected."));
1145
+ return;
1146
+ }
1147
+ // Parse comma-separated numbers
1148
+ const indices = input
1149
+ .split(",")
1150
+ .map((s) => parseInt(s.trim(), 10) - 1)
1151
+ .filter((i) => i >= 0 && i < personas.length);
1152
+ const unique = [...new Set(indices)];
1153
+ if (unique.length === 0) {
1154
+ console.log(chalk.yellow(" No valid selections."));
1155
+ return;
1156
+ }
1157
+ console.log();
1158
+ // Copy framework files first
1159
+ await copyTemplateFiles(teammatesDir);
1160
+ const created = [];
1161
+ for (const idx of unique) {
1162
+ const p = personas[idx];
1163
+ const nameInput = await this.askInput(`Name for ${p.persona} [${p.alias}]: `);
1164
+ const name = nameInput || p.alias;
1165
+ const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
1166
+ await scaffoldFromPersona(teammatesDir, folderName, p);
1167
+ created.push(folderName);
1168
+ console.log(chalk.green(" ✔ ") +
1169
+ chalk.white(`@${folderName}`) +
1170
+ chalk.gray(` — ${p.persona}`));
1171
+ }
1172
+ console.log();
1173
+ console.log(chalk.green(` ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `) + chalk.white(created.map((n) => `@${n}`).join(", ")));
1174
+ console.log(chalk.gray(" Tip: Your agent will adapt ownership and capabilities to this codebase on first task."));
1175
+ console.log();
1176
+ }
1177
+ /**
1178
+ * In-TUI persona picker for /init pick. Uses feedLine + askInline instead
1179
+ * of console.log + askInput.
1180
+ */
1181
+ async runPersonaOnboardingInline(teammatesDir) {
1182
+ const personas = await loadPersonas();
1183
+ if (personas.length === 0) {
1184
+ this.feedLine(tp.warning(" No persona templates found."));
1185
+ this.refreshView();
1186
+ return;
1187
+ }
1188
+ // Display personas in the feed
1189
+ this.feedLine(tp.text(" Available personas:\n"));
1190
+ let currentTier = 0;
1191
+ for (let i = 0; i < personas.length; i++) {
1192
+ const p = personas[i];
1193
+ if (p.tier !== currentTier) {
1194
+ currentTier = p.tier;
1195
+ const label = currentTier === 1 ? "Core" : "Specialized";
1196
+ this.feedLine(tp.muted(` ── ${label} ──`));
1197
+ }
1198
+ const num = String(i + 1).padStart(2, " ");
1199
+ this.feedLine(concat(tp.text(` ${num}) ${p.persona} `), tp.muted(`(${p.alias}) — ${p.description}`)));
1200
+ }
1201
+ this.feedLine(tp.muted("\n Enter numbers separated by commas, e.g. 1,3,5"));
1202
+ this.refreshView();
1203
+ const input = await this.askInline("Personas: ");
1204
+ if (!input) {
1205
+ this.feedLine(tp.muted(" No personas selected."));
1206
+ this.refreshView();
1207
+ return;
1208
+ }
1209
+ const indices = input
1210
+ .split(",")
1211
+ .map((s) => parseInt(s.trim(), 10) - 1)
1212
+ .filter((i) => i >= 0 && i < personas.length);
1213
+ const unique = [...new Set(indices)];
1214
+ if (unique.length === 0) {
1215
+ this.feedLine(tp.warning(" No valid selections."));
1216
+ this.refreshView();
1217
+ return;
1218
+ }
1219
+ await copyTemplateFiles(teammatesDir);
1220
+ const created = [];
1221
+ for (const idx of unique) {
1222
+ const p = personas[idx];
1223
+ const nameInput = await this.askInline(`Name for ${p.persona} [${p.alias}]: `);
1224
+ const name = nameInput || p.alias;
1225
+ const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
1226
+ await scaffoldFromPersona(teammatesDir, folderName, p);
1227
+ created.push(folderName);
1228
+ this.feedLine(concat(tp.success(` ✔ @${folderName}`), tp.muted(` — ${p.persona}`)));
1229
+ }
1230
+ this.feedLine(concat(tp.success(`\n ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `), tp.text(created.map((n) => `@${n}`).join(", "))));
1231
+ this.refreshView();
1232
+ }
998
1233
  /**
999
1234
  * Run the onboarding agent to analyze the codebase and create teammates.
1000
1235
  * Used by both promptOnboarding (pre-orchestrator) and cmdInit (post-orchestrator).
@@ -1404,20 +1639,24 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1404
1639
  chalk.cyan(`@${login}`) +
1405
1640
  (name && name !== login ? chalk.gray(` (${name})`) : ""));
1406
1641
  console.log();
1407
- // Ask for role (optional) since GitHub doesn't provide this
1642
+ // Ask for remaining fields since GitHub doesn't provide them
1408
1643
  const role = await this.askInput("Your role (optional, press Enter to skip): ");
1644
+ const experience = await this.askInput("Relevant experience (e.g., 10 years Go, new to React): ");
1645
+ const preferences = await this.askInput("How you like to work (e.g., terse responses): ");
1646
+ // Auto-detect timezone
1647
+ const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1648
+ const timezone = await this.askInput(`Primary timezone${detectedTz ? ` [${detectedTz}]` : ""}: `);
1409
1649
  const answers = {
1410
1650
  alias: login,
1411
1651
  name: name || login,
1412
1652
  role: role || "",
1413
- experience: "",
1414
- preferences: "",
1415
- context: "",
1653
+ experience: experience || "",
1654
+ preferences: preferences || "",
1655
+ timezone: timezone || detectedTz || "",
1416
1656
  };
1417
1657
  this.writeUserProfile(teammatesDir, login, answers);
1418
1658
  this.createUserAvatar(teammatesDir, login, answers);
1419
- console.log(chalk.green(" ✔ ") +
1420
- chalk.gray(`Profile created — avatar @${login}`));
1659
+ console.log(chalk.green(" ✔ ") + chalk.gray(`Profile created — avatar @${login}`));
1421
1660
  console.log();
1422
1661
  }
1423
1662
  /**
@@ -1427,7 +1666,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1427
1666
  console.log();
1428
1667
  console.log(chalk.gray(" (alias is required, press Enter to skip others)\n"));
1429
1668
  const aliasRaw = await this.askInput("Your alias (e.g., alex): ");
1430
- const alias = aliasRaw.toLowerCase().replace(/[^a-z0-9_-]/g, "").trim();
1669
+ const alias = aliasRaw
1670
+ .toLowerCase()
1671
+ .replace(/[^a-z0-9_-]/g, "")
1672
+ .trim();
1431
1673
  if (!alias) {
1432
1674
  console.log(chalk.yellow(" Alias is required. Run /user to try again.\n"));
1433
1675
  return;
@@ -1436,20 +1678,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1436
1678
  const role = await this.askInput("Your role (e.g., senior backend engineer): ");
1437
1679
  const experience = await this.askInput("Relevant experience (e.g., 10 years Go, new to React): ");
1438
1680
  const preferences = await this.askInput("How you like to work (e.g., terse responses): ");
1439
- const context = await this.askInput("Anything else: ");
1681
+ // Auto-detect timezone
1682
+ const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1683
+ const timezone = await this.askInput(`Primary timezone${detectedTz ? ` [${detectedTz}]` : ""}: `);
1440
1684
  const answers = {
1441
1685
  alias,
1442
1686
  name,
1443
1687
  role,
1444
1688
  experience,
1445
1689
  preferences,
1446
- context,
1690
+ timezone: timezone || detectedTz || "",
1447
1691
  };
1448
1692
  this.writeUserProfile(teammatesDir, alias, answers);
1449
1693
  this.createUserAvatar(teammatesDir, alias, answers);
1450
1694
  console.log();
1451
- console.log(chalk.green(" ✔ ") +
1452
- chalk.gray(`Profile created — avatar @${alias}`));
1695
+ console.log(chalk.green(" ✔ ") + chalk.gray(`Profile created — avatar @${alias}`));
1453
1696
  console.log(chalk.gray(" Update anytime with /user"));
1454
1697
  console.log();
1455
1698
  }
@@ -1464,7 +1707,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1464
1707
  lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
1465
1708
  lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
1466
1709
  lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
1467
- lines.push(`- **Context:** ${answers.context || "_not provided_"}`);
1710
+ lines.push(`- **Primary Timezone:** ${answers.timezone || "_not provided_"}`);
1468
1711
  writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
1469
1712
  }
1470
1713
  /**
@@ -1477,10 +1720,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1477
1720
  mkdirSync(avatarDir, { recursive: true });
1478
1721
  mkdirSync(memoryDir, { recursive: true });
1479
1722
  const name = answers.name || alias;
1480
- const role = answers.role || "Team member";
1723
+ const role = answers.role || "I'm a human working on this project";
1481
1724
  const experience = answers.experience || "";
1482
1725
  const preferences = answers.preferences || "";
1483
- const context = answers.context || "";
1726
+ const timezone = answers.timezone || "";
1484
1727
  // Write SOUL.md
1485
1728
  const soulLines = [
1486
1729
  `# ${name}`,
@@ -1495,9 +1738,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1495
1738
  soulLines.push(`**Experience:** ${experience}`);
1496
1739
  if (preferences)
1497
1740
  soulLines.push(`**Preferences:** ${preferences}`);
1498
- if (context) {
1499
- soulLines.push("", "## Context", "", context);
1500
- }
1741
+ if (timezone)
1742
+ soulLines.push(`**Primary Timezone:** ${timezone}`);
1501
1743
  soulLines.push("");
1502
1744
  const soulPath = join(avatarDir, "SOUL.md");
1503
1745
  writeFileSync(soulPath, soulLines.join("\n"), "utf-8");
@@ -1531,19 +1773,23 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1531
1773
  const avatarDir = join(teammatesDir, alias);
1532
1774
  // Read the avatar's SOUL.md if it exists
1533
1775
  let soul = "";
1534
- let role = "Team member";
1776
+ let role = "I'm a human working on this project";
1535
1777
  try {
1536
1778
  soul = readFileSync(join(avatarDir, "SOUL.md"), "utf-8");
1537
1779
  const roleMatch = soul.match(/\*\*Role:\*\*\s*(.+)/);
1538
1780
  if (roleMatch)
1539
1781
  role = roleMatch[1].trim();
1540
1782
  }
1541
- catch { /* avatar folder may not exist yet */ }
1783
+ catch {
1784
+ /* avatar folder may not exist yet */
1785
+ }
1542
1786
  let wisdom = "";
1543
1787
  try {
1544
1788
  wisdom = readFileSync(join(avatarDir, "WISDOM.md"), "utf-8");
1545
1789
  }
1546
- catch { /* ok */ }
1790
+ catch {
1791
+ /* ok */
1792
+ }
1547
1793
  registry.register({
1548
1794
  name: alias,
1549
1795
  type: "human",
@@ -1556,7 +1802,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1556
1802
  routingKeywords: [],
1557
1803
  });
1558
1804
  // Set presence to online (local user is always online)
1559
- this.orchestrator.getAllStatuses().set(alias, { state: "idle", presence: "online" });
1805
+ this.orchestrator
1806
+ .getAllStatuses()
1807
+ .set(alias, { state: "idle", presence: "online" });
1560
1808
  // Update the adapter name so tasks route to the avatar
1561
1809
  this.userAlias = alias;
1562
1810
  }
@@ -1648,9 +1896,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1648
1896
  if (completedArgs > 0)
1649
1897
  return [];
1650
1898
  const lower = partial.toLowerCase();
1651
- return TeammatesREPL.CONFIGURABLE_SERVICES
1652
- .filter((s) => s.startsWith(lower))
1653
- .map((s) => ({
1899
+ return TeammatesREPL.CONFIGURABLE_SERVICES.filter((s) => s.startsWith(lower)).map((s) => ({
1654
1900
  label: s,
1655
1901
  description: `configure ${s}`,
1656
1902
  completion: `/${cmdName} ${s} `,
@@ -1744,12 +1990,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1744
1990
  });
1745
1991
  }
1746
1992
  for (const name of teammates) {
1747
- if (name.toLowerCase().startsWith(lower)) {
1993
+ // For user avatar, display and match using the adapter name alias
1994
+ const display = name === this.userAlias ? this.adapterName : name;
1995
+ if (display.toLowerCase().startsWith(lower)) {
1748
1996
  const t = this.orchestrator.getRegistry().get(name);
1749
1997
  items.push({
1750
- label: `@${name}`,
1998
+ label: `@${display}`,
1751
1999
  description: t?.role ?? "",
1752
- completion: `${before}@${name} ${after.replace(/^\s+/, "")}`,
2000
+ completion: `${before}@${display} ${after.replace(/^\s+/, "")}`,
1753
2001
  });
1754
2002
  }
1755
2003
  }
@@ -1930,7 +2178,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1930
2178
  registry.register({
1931
2179
  name: this.adapterName,
1932
2180
  type: "ai",
1933
- role: "General-purpose coding agent",
2181
+ role: "Coding agent that performs tasks on your behalf.",
1934
2182
  soul: "",
1935
2183
  wisdom: "",
1936
2184
  dailyLogs: [],
@@ -1939,7 +2187,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1939
2187
  routingKeywords: [],
1940
2188
  cwd: dirname(this.teammatesDir),
1941
2189
  });
1942
- this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle", presence: "online" });
2190
+ this.orchestrator
2191
+ .getAllStatuses()
2192
+ .set(this.adapterName, { state: "idle", presence: "online" });
1943
2193
  }
1944
2194
  // Populate roster on the adapter so prompts include team info
1945
2195
  // Exclude the user avatar and adapter fallback — neither is an addressable teammate
@@ -1965,8 +2215,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1965
2215
  borderStyle: (s) => chalk.gray(s),
1966
2216
  colorize: (value) => {
1967
2217
  const validNames = new Set([
1968
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName),
1969
- this.selfName,
2218
+ ...this.orchestrator
2219
+ .listTeammates()
2220
+ .filter((n) => n !== this.adapterName && n !== this.userAlias),
2221
+ this.adapterName,
1970
2222
  "everyone",
1971
2223
  ]);
1972
2224
  return value
@@ -2011,13 +2263,22 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2011
2263
  const reg = this.orchestrator.getRegistry();
2012
2264
  const statuses = this.orchestrator.getAllStatuses();
2013
2265
  const bannerTeammates = [];
2266
+ // Add user avatar first (displayed as adapter name alias)
2267
+ if (this.userAlias) {
2268
+ const up = statuses.get(this.userAlias)?.presence ?? "online";
2269
+ bannerTeammates.push({
2270
+ name: this.adapterName,
2271
+ role: "Coding agent that performs tasks on your behalf.",
2272
+ presence: up,
2273
+ });
2274
+ }
2014
2275
  for (const name of names) {
2015
2276
  const t = reg.get(name);
2016
2277
  const p = statuses.get(name)?.presence ?? "online";
2017
2278
  bannerTeammates.push({ name, role: t?.role ?? "", presence: p });
2018
2279
  }
2019
2280
  const bannerWidget = new AnimatedBanner({
2020
- displayName: `@${this.selfName}`,
2281
+ displayName: `@${this.adapterName}`,
2021
2282
  teammateCount: names.length,
2022
2283
  cwd: process.cwd(),
2023
2284
  teammates: bannerTeammates,
@@ -2048,8 +2309,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2048
2309
  }
2049
2310
  // Colorize @mentions only if they reference a valid teammate or the user
2050
2311
  const validNames = new Set([
2051
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName),
2052
- this.selfName,
2312
+ ...this.orchestrator
2313
+ .listTeammates()
2314
+ .filter((n) => n !== this.adapterName && n !== this.userAlias),
2315
+ this.adapterName,
2053
2316
  "everyone",
2054
2317
  ]);
2055
2318
  const mentionPattern = /@(\w+)/g;
@@ -2339,7 +2602,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2339
2602
  let pm;
2340
2603
  const preMentions = [];
2341
2604
  while ((pm = preMentionRegex.exec(rawLine)) !== null) {
2342
- const name = pm[1];
2605
+ // Remap adapter name alias → user avatar for routing
2606
+ const name = pm[1] === this.adapterName && this.userAlias ? this.selfName : pm[1];
2343
2607
  if (allNames.includes(name) && !preMentions.includes(name)) {
2344
2608
  preMentions.push(name);
2345
2609
  }
@@ -2411,16 +2675,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2411
2675
  }
2412
2676
  // Slash commands
2413
2677
  if (input.startsWith("/")) {
2414
- this.dispatching = true;
2415
2678
  try {
2416
2679
  await this.dispatch(input);
2417
2680
  }
2418
2681
  catch (err) {
2419
2682
  this.feedLine(tp.error(`Error: ${err.message}`));
2420
2683
  }
2421
- finally {
2422
- this.dispatching = false;
2423
- }
2424
2684
  this.refreshView();
2425
2685
  return;
2426
2686
  }
@@ -2436,7 +2696,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2436
2696
  const termWidth = process.stdout.columns || 100;
2437
2697
  this.feedLine();
2438
2698
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2439
- this.feedLine(concat(tp.text(` @${this.selfName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2699
+ this.feedLine(concat(tp.text(` @${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2440
2700
  this.feedLine(` ${process.cwd()}`);
2441
2701
  // Service status rows
2442
2702
  for (const svc of this.serviceStatuses) {
@@ -2455,11 +2715,25 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2455
2715
  // Roster (with presence indicators)
2456
2716
  this.feedLine();
2457
2717
  const statuses = this.orchestrator.getAllStatuses();
2718
+ // Show user avatar first (displayed as adapter name alias)
2719
+ if (this.userAlias) {
2720
+ const up = statuses.get(this.userAlias)?.presence ?? "online";
2721
+ const udot = up === "online"
2722
+ ? tp.success("●")
2723
+ : up === "reachable"
2724
+ ? tp.warning("●")
2725
+ : tp.error("●");
2726
+ this.feedLine(concat(tp.text(" "), udot, tp.accent(` @${this.adapterName.padEnd(14)}`), tp.muted("Coding agent that performs tasks on your behalf.")));
2727
+ }
2458
2728
  for (const name of teammates) {
2459
2729
  const t = registry.get(name);
2460
2730
  if (t) {
2461
2731
  const p = statuses.get(name)?.presence ?? "online";
2462
- 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("●");
2463
2737
  this.feedLine(concat(tp.text(" "), dot, tp.accent(` @${name.padEnd(14)}`), tp.muted(t.role)));
2464
2738
  }
2465
2739
  }
@@ -2652,7 +2926,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2652
2926
  // Get username for confirmation
2653
2927
  let username = "";
2654
2928
  try {
2655
- 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();
2656
2933
  }
2657
2934
  catch {
2658
2935
  // non-critical
@@ -2705,8 +2982,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2705
2982
  {
2706
2983
  name: "init",
2707
2984
  aliases: ["onboard", "setup"],
2708
- usage: "/init [from-path]",
2709
- description: "Set up teammates (or import from another project)",
2985
+ usage: "/init [pick | from-path]",
2986
+ description: "Set up teammates (pick from personas, or import from another project)",
2710
2987
  run: (args) => this.cmdInit(args),
2711
2988
  },
2712
2989
  {
@@ -2802,6 +3079,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2802
3079
  }
2803
3080
  // ─── Event handler ───────────────────────────────────────────────
2804
3081
  handleEvent(event) {
3082
+ // Suppress all events for agents in silent retry
3083
+ const evtAgent = event.type === "task_assigned"
3084
+ ? event.assignment.teammate
3085
+ : event.type === "task_completed"
3086
+ ? event.result.teammate
3087
+ : event.teammate;
3088
+ if (this.silentAgents.has(evtAgent))
3089
+ return;
2805
3090
  switch (event.type) {
2806
3091
  case "task_assigned": {
2807
3092
  // Track this task and start the animated status bar
@@ -2814,104 +3099,27 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2814
3099
  break;
2815
3100
  }
2816
3101
  case "task_completed": {
2817
- // Remove from active tasks
3102
+ // Remove from active tasks and stop spinner.
3103
+ // Result display is deferred to drainAgentQueue() so the defensive
3104
+ // retry can update rawOutput before anything is shown to the user.
2818
3105
  this.activeTasks.delete(event.result.teammate);
2819
3106
  // Stop animation if no more active tasks
2820
3107
  if (this.activeTasks.size === 0) {
2821
3108
  this.stopStatusAnimation();
2822
3109
  }
2823
- // Suppress display for internal summarization tasks
2824
- const activeEntry = this.agentActive.get(event.result.teammate);
2825
- if (activeEntry?.type === "summarize")
2826
- break;
2827
- if (!this.chatView)
2828
- this.input.deactivateAndErase();
2829
- const raw = event.result.rawOutput ?? "";
2830
- // Strip protocol artifacts
2831
- const cleaned = raw
2832
- .replace(/^TO:\s*\S+\s*\n/im, "")
2833
- .replace(/^#\s+.+\n*/m, "")
2834
- .replace(/```handoff\s*\n@\w+\s*\n[\s\S]*?```/g, "")
2835
- .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
2836
- .trim();
2837
- const sizeKB = cleaned ? Buffer.byteLength(cleaned, "utf-8") / 1024 : 0;
2838
- // Header: "teammate: subject"
2839
- const subject = event.result.summary || "Task completed";
2840
- const displayTeammate = event.result.teammate === this.adapterName ? this.selfName : event.result.teammate;
2841
- this.feedLine(concat(tp.accent(`${displayTeammate}: `), tp.text(subject)));
2842
- this.lastCleanedOutput = cleaned;
2843
- if (cleaned) {
2844
- this.feedMarkdown(cleaned);
2845
- }
2846
- else {
2847
- this.feedLine(tp.muted(" (no response text — the agent may have only performed tool actions)"));
2848
- this.feedLine(tp.muted(` Use /debug ${event.result.teammate} to view full output`));
2849
- // Show diagnostic hints for empty responses
2850
- const diag = event.result.diagnostics;
2851
- if (diag) {
2852
- if (diag.exitCode !== 0 && diag.exitCode !== null) {
2853
- this.feedLine(tp.warning(` ⚠ Process exited with code ${diag.exitCode}`));
2854
- }
2855
- if (diag.signal) {
2856
- this.feedLine(tp.warning(` ⚠ Process killed by signal: ${diag.signal}`));
2857
- }
2858
- if (diag.debugFile) {
2859
- this.feedLine(tp.muted(` Debug log: ${diag.debugFile}`));
2860
- }
2861
- }
2862
- }
2863
- // Render handoffs
2864
- const handoffs = event.result.handoffs;
2865
- if (handoffs.length > 0) {
2866
- this.renderHandoffs(event.result.teammate, handoffs);
2867
- }
2868
- // Clickable [reply] [copy] actions after the response
2869
- if (this.chatView && cleaned) {
2870
- const t = theme();
2871
- const teammate = event.result.teammate;
2872
- const replyId = `reply-${teammate}-${Date.now()}`;
2873
- this._replyContexts.set(replyId, { teammate, message: cleaned });
2874
- this.chatView.appendActionList([
2875
- {
2876
- id: replyId,
2877
- normalStyle: this.makeSpan({
2878
- text: " [reply]",
2879
- style: { fg: t.textDim },
2880
- }),
2881
- hoverStyle: this.makeSpan({
2882
- text: " [reply]",
2883
- style: { fg: t.accent },
2884
- }),
2885
- },
2886
- {
2887
- id: "copy",
2888
- normalStyle: this.makeSpan({
2889
- text: " [copy]",
2890
- style: { fg: t.textDim },
2891
- }),
2892
- hoverStyle: this.makeSpan({
2893
- text: " [copy]",
2894
- style: { fg: t.accent },
2895
- }),
2896
- },
2897
- ]);
2898
- }
2899
- this.feedLine();
2900
- // Auto-detect new teammates added during this task
2901
- this.refreshTeammates();
2902
- this.showPrompt();
2903
3110
  break;
2904
3111
  }
2905
- case "error":
3112
+ case "error": {
2906
3113
  this.activeTasks.delete(event.teammate);
2907
3114
  if (this.activeTasks.size === 0)
2908
3115
  this.stopStatusAnimation();
2909
3116
  if (!this.chatView)
2910
3117
  this.input.deactivateAndErase();
2911
- const displayErr = event.teammate === this.adapterName ? this.selfName : event.teammate;
3118
+ const displayErr = event.teammate === this.selfName ? this.adapterName : event.teammate;
2912
3119
  this.feedLine(tp.error(` ✖ ${displayErr}: ${event.error}`));
2913
3120
  this.showPrompt();
2914
3121
  break;
3122
+ }
2915
3123
  }
2916
3124
  }
2917
3125
  async cmdStatus() {
@@ -2920,14 +3128,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2920
3128
  this.feedLine();
2921
3129
  this.feedLine(tp.bold(" Status"));
2922
3130
  this.feedLine(tp.muted(` ${"─".repeat(50)}`));
2923
- // Show user avatar first if present
3131
+ // Show user avatar first if present (displayed as adapter name alias)
2924
3132
  if (this.userAlias) {
2925
3133
  const userStatus = statuses.get(this.userAlias);
2926
3134
  if (userStatus) {
2927
- this.feedLine(concat(tp.success("●"), tp.accent(` @${this.userAlias}`), tp.muted(" (you)")));
2928
- const t = registry.get(this.userAlias);
2929
- if (t)
2930
- this.feedLine(tp.muted(` ${t.role}`));
3135
+ this.feedLine(concat(tp.success("●"), tp.accent(` @${this.adapterName}`), tp.muted(" (you)")));
3136
+ this.feedLine(tp.muted(" Coding agent that performs tasks on your behalf."));
2931
3137
  this.feedLine();
2932
3138
  }
2933
3139
  }
@@ -3070,7 +3276,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3070
3276
  return;
3071
3277
  }
3072
3278
  const removed = this.taskQueue.splice(n - 1, 1)[0];
3073
- const cancelDisplay = removed.teammate === this.adapterName ? this.selfName : removed.teammate;
3279
+ const cancelDisplay = removed.teammate === this.selfName ? this.adapterName : removed.teammate;
3074
3280
  this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
3075
3281
  this.refreshView();
3076
3282
  }
@@ -3106,11 +3312,54 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3106
3312
  const extraContext = entry.type === "btw" || entry.type === "debug"
3107
3313
  ? ""
3108
3314
  : this.buildConversationContext();
3109
- const result = await this.orchestrator.assign({
3315
+ let result = await this.orchestrator.assign({
3110
3316
  teammate: entry.teammate,
3111
3317
  task: entry.task,
3112
3318
  extraContext: extraContext || undefined,
3113
3319
  });
3320
+ // Defensive retry: if the agent produced no text output but exited
3321
+ // successfully, it likely ended its turn with only file edits.
3322
+ // Retry up to 2 times with progressively simpler prompts.
3323
+ const rawText = (result.rawOutput ?? "").trim();
3324
+ if (!rawText &&
3325
+ result.success &&
3326
+ entry.type !== "btw" &&
3327
+ entry.type !== "debug") {
3328
+ this.silentAgents.add(entry.teammate);
3329
+ // Attempt 1: ask the agent to summarize what it did
3330
+ const retry1 = await this.orchestrator.assign({
3331
+ teammate: entry.teammate,
3332
+ task: `You completed the previous task but produced no visible text output. The user cannot see your work without a text response.\n\nOriginal task: ${entry.task}\n\nPlease respond now with a summary of what you did. Do NOT update session or memory files. Do NOT use any tools. Just produce text output.\n\nFormat:\nTO: user\n# <Subject line>\n\n<Body — what you did, key decisions, files changed>`,
3333
+ raw: true,
3334
+ });
3335
+ const retry1Raw = (retry1.rawOutput ?? "").trim();
3336
+ if (retry1Raw) {
3337
+ result = {
3338
+ ...result,
3339
+ rawOutput: retry1.rawOutput,
3340
+ summary: retry1.summary || result.summary,
3341
+ };
3342
+ }
3343
+ else {
3344
+ // Attempt 2: absolute minimum prompt — just ask for one sentence
3345
+ const retry2 = await this.orchestrator.assign({
3346
+ teammate: entry.teammate,
3347
+ task: `Say "Done." followed by one sentence describing what you changed. No tools. No file edits. Just text.`,
3348
+ raw: true,
3349
+ });
3350
+ const retry2Raw = (retry2.rawOutput ?? "").trim();
3351
+ if (retry2Raw) {
3352
+ result = {
3353
+ ...result,
3354
+ rawOutput: retry2.rawOutput,
3355
+ summary: retry2.summary || result.summary,
3356
+ };
3357
+ }
3358
+ }
3359
+ this.silentAgents.delete(entry.teammate);
3360
+ }
3361
+ // Display the (possibly retried) result to the user
3362
+ this.displayTaskResult(result, entry.type);
3114
3363
  // Write debug entry — skip for debug analysis tasks (avoid recursion)
3115
3364
  if (entry.type !== "debug") {
3116
3365
  this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
@@ -3134,7 +3383,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3134
3383
  if (this.activeTasks.size === 0)
3135
3384
  this.stopStatusAnimation();
3136
3385
  const msg = err?.message ?? String(err);
3137
- const displayAgent = agent === this.adapterName ? this.selfName : agent;
3386
+ const displayAgent = agent === this.selfName ? this.adapterName : agent;
3138
3387
  this.feedLine(tp.error(` ✖ @${displayAgent}: ${msg}`));
3139
3388
  this.refreshView();
3140
3389
  }
@@ -3236,7 +3485,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3236
3485
  const teammatesDir = join(cwd, ".teammates");
3237
3486
  await mkdir(teammatesDir, { recursive: true });
3238
3487
  const fromPath = argsStr.trim();
3239
- if (fromPath) {
3488
+ if (fromPath === "pick") {
3489
+ // Persona picker mode: /init pick
3490
+ await this.runPersonaOnboardingInline(teammatesDir);
3491
+ }
3492
+ else if (fromPath) {
3240
3493
  // Import mode: /init <path-to-another-project>
3241
3494
  const resolved = resolve(fromPath);
3242
3495
  let sourceDir;
@@ -3395,8 +3648,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3395
3648
  // Start draining
3396
3649
  this.kickDrain();
3397
3650
  }
3398
- /** Run compaction + recall index update for a single teammate. */
3399
- 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) {
3400
3657
  const teammateDir = join(this.teammatesDir, name);
3401
3658
  if (this.chatView) {
3402
3659
  this.chatView.setProgress(`Compacting ${name}...`);
@@ -3407,8 +3664,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3407
3664
  spinner = ora({ text: `Compacting ${name}...`, color: "cyan" }).start();
3408
3665
  }
3409
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)
3410
3670
  const result = await compactEpisodic(teammateDir, name);
3411
3671
  const parts = [];
3672
+ if (autoResult) {
3673
+ parts.push(`${autoResult.created.length} auto-compacted (budget overflow)`);
3674
+ }
3412
3675
  if (result.weekliesCreated.length > 0) {
3413
3676
  parts.push(`${result.weekliesCreated.length} weekly summaries created`);
3414
3677
  }
@@ -3424,10 +3687,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3424
3687
  if (parts.length === 0) {
3425
3688
  if (spinner)
3426
3689
  spinner.info(`${name}: nothing to compact`);
3427
- if (this.chatView)
3690
+ // Silent: progress bar only; verbose: feed line
3691
+ if (this.chatView && !silent)
3428
3692
  this.feedLine(tp.muted(` ℹ ${name}: nothing to compact`));
3429
3693
  }
3430
3694
  else {
3695
+ // Actual work done — always show in feed
3431
3696
  if (spinner)
3432
3697
  spinner.succeed(`${name}: ${parts.join(", ")}`);
3433
3698
  if (this.chatView)
@@ -3453,7 +3718,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3453
3718
  syncSpinner.succeed(`${name}: index synced`);
3454
3719
  if (this.chatView) {
3455
3720
  this.chatView.setProgress(null);
3456
- this.feedLine(tp.success(` ✔ ${name}: index synced`));
3721
+ if (!silent)
3722
+ this.feedLine(tp.success(` ✔ ${name}: index synced`));
3457
3723
  }
3458
3724
  }
3459
3725
  catch {
@@ -3469,7 +3735,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3469
3735
  teammate: name,
3470
3736
  task: wisdomPrompt,
3471
3737
  });
3472
- if (this.chatView)
3738
+ if (this.chatView && !silent)
3473
3739
  this.feedLine(tp.muted(` ↻ ${name}: queued wisdom distillation`));
3474
3740
  }
3475
3741
  }
@@ -3483,6 +3749,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3483
3749
  spinner.fail(`${name}: ${msg}`);
3484
3750
  if (this.chatView) {
3485
3751
  this.chatView.setProgress(null);
3752
+ // Errors always show in feed
3486
3753
  this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3487
3754
  }
3488
3755
  }
@@ -3517,34 +3784,34 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3517
3784
  this.refreshView();
3518
3785
  return;
3519
3786
  }
3520
- 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.
3521
-
3522
- Produce a response with these four sections:
3523
-
3524
- ## 1. What's Working
3525
- 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.
3526
-
3527
- ## 2. What's Not Working
3528
- Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
3529
-
3530
- ## 3. Proposed SOUL.md Changes
3531
- The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
3532
-
3533
- **Proposal N: <short title>**
3534
- - **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
3535
- - **Before:** <the current text to replace, or "(new entry)" if adding>
3536
- - **After:** <the exact replacement text>
3537
- - **Why:** <evidence from recent work justifying the change>
3538
-
3539
- Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
3540
-
3541
- ## 4. Questions for the Team
3542
- Issues that can't be resolved unilaterally — they need input from other teammates or the user.
3543
-
3544
- **Rules:**
3545
- - This is a self-review of YOUR work. Do not evaluate other teammates.
3546
- - Evidence over opinion — cite specific examples.
3547
- - 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.
3548
3815
  - Number each proposal (Proposal 1, Proposal 2, etc.) so the user can approve or reject individually.`;
3549
3816
  const label = targets.length > 1
3550
3817
  ? targets.map((n) => `@${n}`).join(", ")
@@ -3609,36 +3876,35 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3609
3876
  .filter((n) => n !== this.selfName && n !== this.adapterName);
3610
3877
  if (teammates.length === 0)
3611
3878
  return;
3612
- // 1. Check each teammate for stale daily logs (older than 7 days)
3613
- const oneWeekAgo = new Date();
3614
- oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
3615
- const cutoff = oneWeekAgo.toISOString().slice(0, 10); // YYYY-MM-DD
3616
- 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
3617
3881
  for (const name of teammates) {
3618
- const memoryDir = join(this.teammatesDir, name, "memory");
3619
- try {
3620
- const entries = await readdir(memoryDir);
3621
- const hasStale = entries.some((e) => {
3622
- if (!e.endsWith(".md"))
3623
- return false;
3624
- const stem = e.replace(".md", "");
3625
- return /^\d{4}-\d{2}-\d{2}$/.test(stem) && stem < cutoff;
3626
- });
3627
- if (hasStale)
3628
- needsCompact.push(name);
3629
- }
3630
- catch {
3631
- /* no memory dir */
3882
+ if (this.chatView) {
3883
+ this.chatView.setProgress(`Maintaining @${name}...`);
3884
+ this.refreshView();
3632
3885
  }
3886
+ await this.runCompact(name, true);
3633
3887
  }
3634
- if (needsCompact.length > 0) {
3635
- 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);
3636
3890
  this.refreshView();
3637
- for (const name of needsCompact) {
3638
- 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 */
3639
3905
  }
3640
3906
  }
3641
- // 2. Sync recall indexes (bundled library call)
3907
+ // 3. Sync recall indexes (bundled library call)
3642
3908
  try {
3643
3909
  await syncRecallIndex(this.teammatesDir);
3644
3910
  }
@@ -3777,7 +4043,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3777
4043
  // Has args — queue a task to apply the change
3778
4044
  const task = `Update the file ${userMdPath} with the following change:\n\n${change}\n\nKeep the existing content intact unless the change explicitly replaces something. This is the user's profile — be concise and accurate.`;
3779
4045
  this.taskQueue.push({ type: "agent", teammate: this.selfName, task });
3780
- this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.selfName}`)));
4046
+ this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.adapterName}`)));
3781
4047
  this.feedLine();
3782
4048
  this.refreshView();
3783
4049
  this.kickDrain();
@@ -3794,7 +4060,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3794
4060
  teammate: this.selfName,
3795
4061
  task: question,
3796
4062
  });
3797
- this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.selfName}`)));
4063
+ this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.adapterName}`)));
3798
4064
  this.feedLine();
3799
4065
  this.refreshView();
3800
4066
  this.kickDrain();