@teammates/cli 0.4.1 → 0.5.0

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
@@ -23,6 +23,7 @@ import { buildWisdomPrompt, compactEpisodic } 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";
26
27
  import { Orchestrator } from "./orchestrator.js";
27
28
  import { colorToHex, theme, tp } from "./theme.js";
28
29
  // ─── Parsed CLI arguments ────────────────────────────────────────────
@@ -48,6 +49,108 @@ 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
73
+ ? this.adapterName
74
+ : result.teammate;
75
+ this.feedLine(concat(tp.accent(`${displayTeammate}: `), tp.text(subject)));
76
+ this.lastCleanedOutput = cleaned;
77
+ if (cleaned) {
78
+ this.feedMarkdown(cleaned);
79
+ }
80
+ else if (result.changedFiles.length > 0 || result.summary) {
81
+ // Agent produced no body text but DID do work — generate a synthetic
82
+ // summary from available metadata so the user sees something useful.
83
+ const syntheticLines = [];
84
+ if (result.summary) {
85
+ syntheticLines.push(result.summary);
86
+ }
87
+ if (result.changedFiles.length > 0) {
88
+ syntheticLines.push("");
89
+ syntheticLines.push("**Files changed:**");
90
+ for (const f of result.changedFiles) {
91
+ syntheticLines.push(`- ${f}`);
92
+ }
93
+ }
94
+ this.feedMarkdown(syntheticLines.join("\n"));
95
+ }
96
+ else {
97
+ this.feedLine(tp.muted(" (no response text — the agent may have only performed tool actions)"));
98
+ this.feedLine(tp.muted(` Use /debug ${result.teammate} to view full output`));
99
+ // Show diagnostic hints for empty responses
100
+ const diag = result.diagnostics;
101
+ if (diag) {
102
+ if (diag.exitCode !== 0 && diag.exitCode !== null) {
103
+ this.feedLine(tp.warning(` ⚠ Process exited with code ${diag.exitCode}`));
104
+ }
105
+ if (diag.signal) {
106
+ this.feedLine(tp.warning(` ⚠ Process killed by signal: ${diag.signal}`));
107
+ }
108
+ if (diag.debugFile) {
109
+ this.feedLine(tp.muted(` Debug log: ${diag.debugFile}`));
110
+ }
111
+ }
112
+ }
113
+ // Render handoffs
114
+ const handoffs = result.handoffs;
115
+ if (handoffs.length > 0) {
116
+ this.renderHandoffs(result.teammate, handoffs);
117
+ }
118
+ // Clickable [reply] [copy] actions after the response
119
+ if (this.chatView && cleaned) {
120
+ const t = theme();
121
+ const teammate = result.teammate;
122
+ const replyId = `reply-${teammate}-${Date.now()}`;
123
+ this._replyContexts.set(replyId, { teammate, message: cleaned });
124
+ this.chatView.appendActionList([
125
+ {
126
+ id: replyId,
127
+ normalStyle: this.makeSpan({
128
+ text: " [reply]",
129
+ style: { fg: t.textDim },
130
+ }),
131
+ hoverStyle: this.makeSpan({
132
+ text: " [reply]",
133
+ style: { fg: t.accent },
134
+ }),
135
+ },
136
+ {
137
+ id: "copy",
138
+ normalStyle: this.makeSpan({
139
+ text: " [copy]",
140
+ style: { fg: t.textDim },
141
+ }),
142
+ hoverStyle: this.makeSpan({
143
+ text: " [copy]",
144
+ style: { fg: t.accent },
145
+ }),
146
+ },
147
+ ]);
148
+ }
149
+ this.feedLine();
150
+ // Auto-detect new teammates added during this task
151
+ this.refreshTeammates();
152
+ this.showPrompt();
153
+ }
51
154
  /** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
52
155
  static CONV_HISTORY_CHARS = 24_000 * 4;
53
156
  buildConversationContext() {
@@ -116,6 +219,8 @@ class TeammatesREPL {
116
219
  taskQueue = [];
117
220
  /** Per-agent active tasks — one per agent running in parallel. */
118
221
  agentActive = new Map();
222
+ /** Agents currently in a silent retry — suppress all events. */
223
+ silentAgents = new Set();
119
224
  /** Per-agent drain locks — prevents double-draining a single agent. */
120
225
  agentDrainLocks = new Map();
121
226
  /** Stored pasted text keyed by paste number, expanded on Enter. */
@@ -235,7 +340,7 @@ class TeammatesREPL {
235
340
  const entries = Array.from(this.activeTasks.values());
236
341
  const idx = this.statusRotateIndex % entries.length;
237
342
  const { teammate, task } = entries[idx];
238
- const displayName = teammate === this.adapterName ? this.selfName : teammate;
343
+ const displayName = teammate === this.selfName ? this.adapterName : teammate;
239
344
  const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
240
345
  const taskPreview = task.length > 50 ? `${task.slice(0, 47)}...` : task;
241
346
  const queueInfo = this.activeTasks.size > 1 ? ` (${idx + 1}/${this.activeTasks.size})` : "";
@@ -891,7 +996,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
891
996
  let m;
892
997
  mentioned = [];
893
998
  while ((m = mentionRegex.exec(input)) !== null) {
894
- const name = m[1];
999
+ // Remap adapter name alias → user avatar for routing
1000
+ const name = (m[1] === this.adapterName && this.userAlias) ? this.selfName : m[1];
895
1001
  if (allNames.includes(name) && !mentioned.includes(name)) {
896
1002
  mentioned.push(name);
897
1003
  }
@@ -921,7 +1027,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
921
1027
  {
922
1028
  const bg = this._userBg;
923
1029
  const t = theme();
924
- const displayName = match === this.adapterName ? this.selfName : match;
1030
+ const displayName = match === this.selfName ? this.adapterName : match;
925
1031
  this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${displayName}`)));
926
1032
  }
927
1033
  this.feedLine();
@@ -959,42 +1065,173 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
959
1065
  console.log(chalk.white(" Set up teammates for this project?\n"));
960
1066
  console.log(chalk.cyan(" 1") +
961
1067
  chalk.gray(") ") +
962
- chalk.white("New team") +
963
- chalk.gray(" — analyze this codebase and create teammates from scratch"));
1068
+ chalk.white("Pick teammates") +
1069
+ chalk.gray(" — choose from persona templates"));
964
1070
  console.log(chalk.cyan(" 2") +
1071
+ chalk.gray(") ") +
1072
+ chalk.white("Auto-generate") +
1073
+ chalk.gray(" — let your agent analyze the codebase and create teammates"));
1074
+ console.log(chalk.cyan(" 3") +
965
1075
  chalk.gray(") ") +
966
1076
  chalk.white("Import team") +
967
1077
  chalk.gray(" — copy teammates from another project"));
968
- console.log(chalk.cyan(" 3") +
1078
+ console.log(chalk.cyan(" 4") +
969
1079
  chalk.gray(") ") +
970
1080
  chalk.white("Solo mode") +
971
1081
  chalk.gray(" — use your agent without teammates"));
972
- console.log(chalk.cyan(" 4") + chalk.gray(") ") + chalk.white("Exit"));
1082
+ console.log(chalk.cyan(" 5") + chalk.gray(") ") + chalk.white("Exit"));
973
1083
  console.log();
974
- const choice = await this.askChoice("Pick an option (1/2/3/4): ", [
1084
+ const choice = await this.askChoice("Pick an option (1/2/3/4/5): ", [
975
1085
  "1",
976
1086
  "2",
977
1087
  "3",
978
1088
  "4",
1089
+ "5",
979
1090
  ]);
980
- if (choice === "4") {
1091
+ if (choice === "5") {
981
1092
  console.log(chalk.gray(" Goodbye."));
982
1093
  return false;
983
1094
  }
984
- if (choice === "3") {
1095
+ if (choice === "4") {
985
1096
  console.log(chalk.gray(" Running in solo mode — all tasks go to your agent."));
986
1097
  console.log(chalk.gray(" Run /init later to set up teammates."));
987
1098
  console.log();
988
1099
  return true;
989
1100
  }
990
- if (choice === "2") {
1101
+ if (choice === "3") {
991
1102
  await this.runImport(cwd);
992
1103
  return true;
993
1104
  }
994
- // choice === "1": Run onboarding via the agent
995
- await this.runOnboardingAgent(adapter, cwd);
1105
+ if (choice === "2") {
1106
+ // Auto-generate via agent
1107
+ await this.runOnboardingAgent(adapter, cwd);
1108
+ return true;
1109
+ }
1110
+ // choice === "1": Pick from persona templates
1111
+ await this.runPersonaOnboarding(teammatesDir);
996
1112
  return true;
997
1113
  }
1114
+ /**
1115
+ * Persona-based onboarding: show a list of bundled personas, let the user
1116
+ * pick which ones to create, optionally rename them, and scaffold the folders.
1117
+ */
1118
+ async runPersonaOnboarding(teammatesDir) {
1119
+ const personas = await loadPersonas();
1120
+ if (personas.length === 0) {
1121
+ console.log(chalk.yellow(" No persona templates found."));
1122
+ return;
1123
+ }
1124
+ console.log();
1125
+ console.log(chalk.white(" Available personas:\n"));
1126
+ // Display personas grouped by tier
1127
+ let currentTier = 0;
1128
+ for (let i = 0; i < personas.length; i++) {
1129
+ const p = personas[i];
1130
+ if (p.tier !== currentTier) {
1131
+ currentTier = p.tier;
1132
+ const label = currentTier === 1 ? "Core" : "Specialized";
1133
+ console.log(chalk.gray(` ── ${label} ──`));
1134
+ }
1135
+ const num = String(i + 1).padStart(2, " ");
1136
+ console.log(chalk.cyan(` ${num}`) +
1137
+ chalk.gray(") ") +
1138
+ chalk.white(p.persona) +
1139
+ chalk.gray(` (${p.alias})`) +
1140
+ chalk.gray(` — ${p.description}`));
1141
+ }
1142
+ console.log();
1143
+ console.log(chalk.gray(" Enter numbers separated by commas, e.g. 1,3,5"));
1144
+ console.log();
1145
+ const input = await this.askInput("Personas: ");
1146
+ if (!input) {
1147
+ console.log(chalk.gray(" No personas selected."));
1148
+ return;
1149
+ }
1150
+ // Parse comma-separated numbers
1151
+ const indices = input
1152
+ .split(",")
1153
+ .map((s) => parseInt(s.trim(), 10) - 1)
1154
+ .filter((i) => i >= 0 && i < personas.length);
1155
+ const unique = [...new Set(indices)];
1156
+ if (unique.length === 0) {
1157
+ console.log(chalk.yellow(" No valid selections."));
1158
+ return;
1159
+ }
1160
+ console.log();
1161
+ // Copy framework files first
1162
+ await copyTemplateFiles(teammatesDir);
1163
+ const created = [];
1164
+ for (const idx of unique) {
1165
+ const p = personas[idx];
1166
+ const nameInput = await this.askInput(`Name for ${p.persona} [${p.alias}]: `);
1167
+ const name = nameInput || p.alias;
1168
+ const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
1169
+ await scaffoldFromPersona(teammatesDir, folderName, p);
1170
+ created.push(folderName);
1171
+ console.log(chalk.green(" ✔ ") + chalk.white(`@${folderName}`) + chalk.gray(` — ${p.persona}`));
1172
+ }
1173
+ console.log();
1174
+ console.log(chalk.green(` ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `) +
1175
+ chalk.white(created.map((n) => `@${n}`).join(", ")));
1176
+ console.log(chalk.gray(" Tip: Your agent will adapt ownership and capabilities to this codebase on first task."));
1177
+ console.log();
1178
+ }
1179
+ /**
1180
+ * In-TUI persona picker for /init pick. Uses feedLine + askInline instead
1181
+ * of console.log + askInput.
1182
+ */
1183
+ async runPersonaOnboardingInline(teammatesDir) {
1184
+ const personas = await loadPersonas();
1185
+ if (personas.length === 0) {
1186
+ this.feedLine(tp.warning(" No persona templates found."));
1187
+ this.refreshView();
1188
+ return;
1189
+ }
1190
+ // Display personas in the feed
1191
+ this.feedLine(tp.text(" Available personas:\n"));
1192
+ let currentTier = 0;
1193
+ for (let i = 0; i < personas.length; i++) {
1194
+ const p = personas[i];
1195
+ if (p.tier !== currentTier) {
1196
+ currentTier = p.tier;
1197
+ const label = currentTier === 1 ? "Core" : "Specialized";
1198
+ this.feedLine(tp.muted(` ── ${label} ──`));
1199
+ }
1200
+ const num = String(i + 1).padStart(2, " ");
1201
+ this.feedLine(concat(tp.text(` ${num}) ${p.persona} `), tp.muted(`(${p.alias}) — ${p.description}`)));
1202
+ }
1203
+ this.feedLine(tp.muted("\n Enter numbers separated by commas, e.g. 1,3,5"));
1204
+ this.refreshView();
1205
+ const input = await this.askInline("Personas: ");
1206
+ if (!input) {
1207
+ this.feedLine(tp.muted(" No personas selected."));
1208
+ this.refreshView();
1209
+ return;
1210
+ }
1211
+ const indices = input
1212
+ .split(",")
1213
+ .map((s) => parseInt(s.trim(), 10) - 1)
1214
+ .filter((i) => i >= 0 && i < personas.length);
1215
+ const unique = [...new Set(indices)];
1216
+ if (unique.length === 0) {
1217
+ this.feedLine(tp.warning(" No valid selections."));
1218
+ this.refreshView();
1219
+ return;
1220
+ }
1221
+ await copyTemplateFiles(teammatesDir);
1222
+ const created = [];
1223
+ for (const idx of unique) {
1224
+ const p = personas[idx];
1225
+ const nameInput = await this.askInline(`Name for ${p.persona} [${p.alias}]: `);
1226
+ const name = nameInput || p.alias;
1227
+ const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
1228
+ await scaffoldFromPersona(teammatesDir, folderName, p);
1229
+ created.push(folderName);
1230
+ this.feedLine(concat(tp.success(` ✔ @${folderName}`), tp.muted(` — ${p.persona}`)));
1231
+ }
1232
+ this.feedLine(concat(tp.success(`\n ✔ Created ${created.length} teammate${created.length > 1 ? "s" : ""}: `), tp.text(created.map((n) => `@${n}`).join(", "))));
1233
+ this.refreshView();
1234
+ }
998
1235
  /**
999
1236
  * Run the onboarding agent to analyze the codebase and create teammates.
1000
1237
  * Used by both promptOnboarding (pre-orchestrator) and cmdInit (post-orchestrator).
@@ -1404,15 +1641,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1404
1641
  chalk.cyan(`@${login}`) +
1405
1642
  (name && name !== login ? chalk.gray(` (${name})`) : ""));
1406
1643
  console.log();
1407
- // Ask for role (optional) since GitHub doesn't provide this
1644
+ // Ask for remaining fields since GitHub doesn't provide them
1408
1645
  const role = await this.askInput("Your role (optional, press Enter to skip): ");
1646
+ const experience = await this.askInput("Relevant experience (e.g., 10 years Go, new to React): ");
1647
+ const preferences = await this.askInput("How you like to work (e.g., terse responses): ");
1648
+ // Auto-detect timezone
1649
+ const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1650
+ const timezone = await this.askInput(`Primary timezone${detectedTz ? ` [${detectedTz}]` : ""}: `);
1409
1651
  const answers = {
1410
1652
  alias: login,
1411
1653
  name: name || login,
1412
1654
  role: role || "",
1413
- experience: "",
1414
- preferences: "",
1415
- context: "",
1655
+ experience: experience || "",
1656
+ preferences: preferences || "",
1657
+ timezone: timezone || detectedTz || "",
1416
1658
  };
1417
1659
  this.writeUserProfile(teammatesDir, login, answers);
1418
1660
  this.createUserAvatar(teammatesDir, login, answers);
@@ -1436,14 +1678,16 @@ 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);
@@ -1464,7 +1708,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1464
1708
  lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
1465
1709
  lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
1466
1710
  lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
1467
- lines.push(`- **Context:** ${answers.context || "_not provided_"}`);
1711
+ lines.push(`- **Primary Timezone:** ${answers.timezone || "_not provided_"}`);
1468
1712
  writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
1469
1713
  }
1470
1714
  /**
@@ -1477,10 +1721,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1477
1721
  mkdirSync(avatarDir, { recursive: true });
1478
1722
  mkdirSync(memoryDir, { recursive: true });
1479
1723
  const name = answers.name || alias;
1480
- const role = answers.role || "Team member";
1724
+ const role = answers.role || "I'm a human working on this project";
1481
1725
  const experience = answers.experience || "";
1482
1726
  const preferences = answers.preferences || "";
1483
- const context = answers.context || "";
1727
+ const timezone = answers.timezone || "";
1484
1728
  // Write SOUL.md
1485
1729
  const soulLines = [
1486
1730
  `# ${name}`,
@@ -1495,9 +1739,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1495
1739
  soulLines.push(`**Experience:** ${experience}`);
1496
1740
  if (preferences)
1497
1741
  soulLines.push(`**Preferences:** ${preferences}`);
1498
- if (context) {
1499
- soulLines.push("", "## Context", "", context);
1500
- }
1742
+ if (timezone)
1743
+ soulLines.push(`**Primary Timezone:** ${timezone}`);
1501
1744
  soulLines.push("");
1502
1745
  const soulPath = join(avatarDir, "SOUL.md");
1503
1746
  writeFileSync(soulPath, soulLines.join("\n"), "utf-8");
@@ -1531,7 +1774,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1531
1774
  const avatarDir = join(teammatesDir, alias);
1532
1775
  // Read the avatar's SOUL.md if it exists
1533
1776
  let soul = "";
1534
- let role = "Team member";
1777
+ let role = "I'm a human working on this project";
1535
1778
  try {
1536
1779
  soul = readFileSync(join(avatarDir, "SOUL.md"), "utf-8");
1537
1780
  const roleMatch = soul.match(/\*\*Role:\*\*\s*(.+)/);
@@ -1744,12 +1987,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1744
1987
  });
1745
1988
  }
1746
1989
  for (const name of teammates) {
1747
- if (name.toLowerCase().startsWith(lower)) {
1990
+ // For user avatar, display and match using the adapter name alias
1991
+ const display = name === this.userAlias ? this.adapterName : name;
1992
+ if (display.toLowerCase().startsWith(lower)) {
1748
1993
  const t = this.orchestrator.getRegistry().get(name);
1749
1994
  items.push({
1750
- label: `@${name}`,
1995
+ label: `@${display}`,
1751
1996
  description: t?.role ?? "",
1752
- completion: `${before}@${name} ${after.replace(/^\s+/, "")}`,
1997
+ completion: `${before}@${display} ${after.replace(/^\s+/, "")}`,
1753
1998
  });
1754
1999
  }
1755
2000
  }
@@ -1930,7 +2175,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1930
2175
  registry.register({
1931
2176
  name: this.adapterName,
1932
2177
  type: "ai",
1933
- role: "General-purpose coding agent",
2178
+ role: "Coding agent that performs tasks on your behalf.",
1934
2179
  soul: "",
1935
2180
  wisdom: "",
1936
2181
  dailyLogs: [],
@@ -1965,8 +2210,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1965
2210
  borderStyle: (s) => chalk.gray(s),
1966
2211
  colorize: (value) => {
1967
2212
  const validNames = new Set([
1968
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName),
1969
- this.selfName,
2213
+ ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
2214
+ this.adapterName,
1970
2215
  "everyone",
1971
2216
  ]);
1972
2217
  return value
@@ -2011,13 +2256,19 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2011
2256
  const reg = this.orchestrator.getRegistry();
2012
2257
  const statuses = this.orchestrator.getAllStatuses();
2013
2258
  const bannerTeammates = [];
2259
+ // Add user avatar first (displayed as adapter name alias)
2260
+ if (this.userAlias) {
2261
+ const ut = reg.get(this.userAlias);
2262
+ 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 });
2264
+ }
2014
2265
  for (const name of names) {
2015
2266
  const t = reg.get(name);
2016
2267
  const p = statuses.get(name)?.presence ?? "online";
2017
2268
  bannerTeammates.push({ name, role: t?.role ?? "", presence: p });
2018
2269
  }
2019
2270
  const bannerWidget = new AnimatedBanner({
2020
- displayName: `@${this.selfName}`,
2271
+ displayName: `@${this.adapterName}`,
2021
2272
  teammateCount: names.length,
2022
2273
  cwd: process.cwd(),
2023
2274
  teammates: bannerTeammates,
@@ -2048,8 +2299,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2048
2299
  }
2049
2300
  // Colorize @mentions only if they reference a valid teammate or the user
2050
2301
  const validNames = new Set([
2051
- ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName),
2052
- this.selfName,
2302
+ ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
2303
+ this.adapterName,
2053
2304
  "everyone",
2054
2305
  ]);
2055
2306
  const mentionPattern = /@(\w+)/g;
@@ -2339,7 +2590,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2339
2590
  let pm;
2340
2591
  const preMentions = [];
2341
2592
  while ((pm = preMentionRegex.exec(rawLine)) !== null) {
2342
- const name = pm[1];
2593
+ // Remap adapter name alias → user avatar for routing
2594
+ const name = (pm[1] === this.adapterName && this.userAlias) ? this.selfName : pm[1];
2343
2595
  if (allNames.includes(name) && !preMentions.includes(name)) {
2344
2596
  preMentions.push(name);
2345
2597
  }
@@ -2436,7 +2688,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2436
2688
  const termWidth = process.stdout.columns || 100;
2437
2689
  this.feedLine();
2438
2690
  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"}`)));
2691
+ this.feedLine(concat(tp.text(` @${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2440
2692
  this.feedLine(` ${process.cwd()}`);
2441
2693
  // Service status rows
2442
2694
  for (const svc of this.serviceStatuses) {
@@ -2455,6 +2707,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2455
2707
  // Roster (with presence indicators)
2456
2708
  this.feedLine();
2457
2709
  const statuses = this.orchestrator.getAllStatuses();
2710
+ // Show user avatar first (displayed as adapter name alias)
2711
+ if (this.userAlias) {
2712
+ const up = statuses.get(this.userAlias)?.presence ?? "online";
2713
+ const udot = up === "online" ? tp.success("●") : up === "reachable" ? tp.warning("●") : tp.error("●");
2714
+ this.feedLine(concat(tp.text(" "), udot, tp.accent(` @${this.adapterName.padEnd(14)}`), tp.muted("Coding agent that performs tasks on your behalf.")));
2715
+ }
2458
2716
  for (const name of teammates) {
2459
2717
  const t = registry.get(name);
2460
2718
  if (t) {
@@ -2705,8 +2963,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2705
2963
  {
2706
2964
  name: "init",
2707
2965
  aliases: ["onboard", "setup"],
2708
- usage: "/init [from-path]",
2709
- description: "Set up teammates (or import from another project)",
2966
+ usage: "/init [pick | from-path]",
2967
+ description: "Set up teammates (pick from personas, or import from another project)",
2710
2968
  run: (args) => this.cmdInit(args),
2711
2969
  },
2712
2970
  {
@@ -2802,6 +3060,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2802
3060
  }
2803
3061
  // ─── Event handler ───────────────────────────────────────────────
2804
3062
  handleEvent(event) {
3063
+ // Suppress all events for agents in silent retry
3064
+ const evtAgent = event.type === "task_assigned"
3065
+ ? event.assignment.teammate
3066
+ : event.type === "task_completed"
3067
+ ? event.result.teammate
3068
+ : event.teammate;
3069
+ if (this.silentAgents.has(evtAgent))
3070
+ return;
2805
3071
  switch (event.type) {
2806
3072
  case "task_assigned": {
2807
3073
  // Track this task and start the animated status bar
@@ -2814,92 +3080,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2814
3080
  break;
2815
3081
  }
2816
3082
  case "task_completed": {
2817
- // Remove from active tasks
3083
+ // Remove from active tasks and stop spinner.
3084
+ // Result display is deferred to drainAgentQueue() so the defensive
3085
+ // retry can update rawOutput before anything is shown to the user.
2818
3086
  this.activeTasks.delete(event.result.teammate);
2819
3087
  // Stop animation if no more active tasks
2820
3088
  if (this.activeTasks.size === 0) {
2821
3089
  this.stopStatusAnimation();
2822
3090
  }
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
3091
  break;
2904
3092
  }
2905
3093
  case "error":
@@ -2908,7 +3096,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2908
3096
  this.stopStatusAnimation();
2909
3097
  if (!this.chatView)
2910
3098
  this.input.deactivateAndErase();
2911
- const displayErr = event.teammate === this.adapterName ? this.selfName : event.teammate;
3099
+ const displayErr = event.teammate === this.selfName ? this.adapterName : event.teammate;
2912
3100
  this.feedLine(tp.error(` ✖ ${displayErr}: ${event.error}`));
2913
3101
  this.showPrompt();
2914
3102
  break;
@@ -2920,14 +3108,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2920
3108
  this.feedLine();
2921
3109
  this.feedLine(tp.bold(" Status"));
2922
3110
  this.feedLine(tp.muted(` ${"─".repeat(50)}`));
2923
- // Show user avatar first if present
3111
+ // Show user avatar first if present (displayed as adapter name alias)
2924
3112
  if (this.userAlias) {
2925
3113
  const userStatus = statuses.get(this.userAlias);
2926
3114
  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}`));
3115
+ this.feedLine(concat(tp.success("●"), tp.accent(` @${this.adapterName}`), tp.muted(" (you)")));
3116
+ this.feedLine(tp.muted(" Coding agent that performs tasks on your behalf."));
2931
3117
  this.feedLine();
2932
3118
  }
2933
3119
  }
@@ -3070,7 +3256,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3070
3256
  return;
3071
3257
  }
3072
3258
  const removed = this.taskQueue.splice(n - 1, 1)[0];
3073
- const cancelDisplay = removed.teammate === this.adapterName ? this.selfName : removed.teammate;
3259
+ const cancelDisplay = removed.teammate === this.selfName ? this.adapterName : removed.teammate;
3074
3260
  this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
3075
3261
  this.refreshView();
3076
3262
  }
@@ -3106,11 +3292,54 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3106
3292
  const extraContext = entry.type === "btw" || entry.type === "debug"
3107
3293
  ? ""
3108
3294
  : this.buildConversationContext();
3109
- const result = await this.orchestrator.assign({
3295
+ let result = await this.orchestrator.assign({
3110
3296
  teammate: entry.teammate,
3111
3297
  task: entry.task,
3112
3298
  extraContext: extraContext || undefined,
3113
3299
  });
3300
+ // Defensive retry: if the agent produced no text output but exited
3301
+ // successfully, it likely ended its turn with only file edits.
3302
+ // Retry up to 2 times with progressively simpler prompts.
3303
+ const rawText = (result.rawOutput ?? "").trim();
3304
+ if (!rawText &&
3305
+ result.success &&
3306
+ entry.type !== "btw" &&
3307
+ entry.type !== "debug") {
3308
+ this.silentAgents.add(entry.teammate);
3309
+ // Attempt 1: ask the agent to summarize what it did
3310
+ const retry1 = await this.orchestrator.assign({
3311
+ teammate: entry.teammate,
3312
+ 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>`,
3313
+ raw: true,
3314
+ });
3315
+ const retry1Raw = (retry1.rawOutput ?? "").trim();
3316
+ if (retry1Raw) {
3317
+ result = {
3318
+ ...result,
3319
+ rawOutput: retry1.rawOutput,
3320
+ summary: retry1.summary || result.summary,
3321
+ };
3322
+ }
3323
+ else {
3324
+ // Attempt 2: absolute minimum prompt — just ask for one sentence
3325
+ const retry2 = await this.orchestrator.assign({
3326
+ teammate: entry.teammate,
3327
+ task: `Say "Done." followed by one sentence describing what you changed. No tools. No file edits. Just text.`,
3328
+ raw: true,
3329
+ });
3330
+ const retry2Raw = (retry2.rawOutput ?? "").trim();
3331
+ if (retry2Raw) {
3332
+ result = {
3333
+ ...result,
3334
+ rawOutput: retry2.rawOutput,
3335
+ summary: retry2.summary || result.summary,
3336
+ };
3337
+ }
3338
+ }
3339
+ this.silentAgents.delete(entry.teammate);
3340
+ }
3341
+ // Display the (possibly retried) result to the user
3342
+ this.displayTaskResult(result, entry.type);
3114
3343
  // Write debug entry — skip for debug analysis tasks (avoid recursion)
3115
3344
  if (entry.type !== "debug") {
3116
3345
  this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
@@ -3134,7 +3363,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3134
3363
  if (this.activeTasks.size === 0)
3135
3364
  this.stopStatusAnimation();
3136
3365
  const msg = err?.message ?? String(err);
3137
- const displayAgent = agent === this.adapterName ? this.selfName : agent;
3366
+ const displayAgent = agent === this.selfName ? this.adapterName : agent;
3138
3367
  this.feedLine(tp.error(` ✖ @${displayAgent}: ${msg}`));
3139
3368
  this.refreshView();
3140
3369
  }
@@ -3236,7 +3465,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3236
3465
  const teammatesDir = join(cwd, ".teammates");
3237
3466
  await mkdir(teammatesDir, { recursive: true });
3238
3467
  const fromPath = argsStr.trim();
3239
- if (fromPath) {
3468
+ if (fromPath === "pick") {
3469
+ // Persona picker mode: /init pick
3470
+ await this.runPersonaOnboardingInline(teammatesDir);
3471
+ }
3472
+ else if (fromPath) {
3240
3473
  // Import mode: /init <path-to-another-project>
3241
3474
  const resolved = resolve(fromPath);
3242
3475
  let sourceDir;
@@ -3777,7 +4010,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3777
4010
  // Has args — queue a task to apply the change
3778
4011
  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
4012
  this.taskQueue.push({ type: "agent", teammate: this.selfName, task });
3780
- this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.selfName}`)));
4013
+ this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.adapterName}`)));
3781
4014
  this.feedLine();
3782
4015
  this.refreshView();
3783
4016
  this.kickDrain();
@@ -3794,7 +4027,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3794
4027
  teammate: this.selfName,
3795
4028
  task: question,
3796
4029
  });
3797
- this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.selfName}`)));
4030
+ this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.adapterName}`)));
3798
4031
  this.feedLine();
3799
4032
  this.refreshView();
3800
4033
  this.kickDrain();