@teammates/cli 0.4.0 → 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.
Files changed (43) hide show
  1. package/README.md +36 -4
  2. package/dist/adapter.d.ts +13 -3
  3. package/dist/adapter.js +48 -11
  4. package/dist/adapter.test.js +1 -0
  5. package/dist/adapters/cli-proxy.d.ts +3 -1
  6. package/dist/adapters/cli-proxy.js +19 -4
  7. package/dist/adapters/copilot.d.ts +3 -1
  8. package/dist/adapters/copilot.js +16 -2
  9. package/dist/adapters/echo.d.ts +3 -1
  10. package/dist/adapters/echo.js +2 -2
  11. package/dist/adapters/echo.test.js +1 -0
  12. package/dist/banner.d.ts +6 -1
  13. package/dist/banner.js +18 -3
  14. package/dist/cli-args.js +0 -1
  15. package/dist/cli.js +914 -346
  16. package/dist/console/startup.d.ts +2 -1
  17. package/dist/console/startup.js +1 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +1 -0
  20. package/dist/orchestrator.d.ts +2 -0
  21. package/dist/orchestrator.js +18 -13
  22. package/dist/orchestrator.test.js +2 -1
  23. package/dist/personas.d.ts +42 -0
  24. package/dist/personas.js +108 -0
  25. package/dist/registry.js +7 -0
  26. package/dist/registry.test.js +1 -0
  27. package/dist/types.d.ts +8 -0
  28. package/package.json +4 -3
  29. package/personas/architect.md +91 -0
  30. package/personas/backend.md +93 -0
  31. package/personas/data-engineer.md +92 -0
  32. package/personas/designer.md +92 -0
  33. package/personas/devops.md +93 -0
  34. package/personas/frontend.md +94 -0
  35. package/personas/ml-ai.md +96 -0
  36. package/personas/mobile.md +93 -0
  37. package/personas/performance.md +92 -0
  38. package/personas/pm.md +89 -0
  39. package/personas/qa.md +92 -0
  40. package/personas/security.md +92 -0
  41. package/personas/sre.md +93 -0
  42. package/personas/swe.md +88 -0
  43. package/personas/tech-writer.md +93 -0
package/dist/cli.js CHANGED
@@ -7,12 +7,12 @@
7
7
  * teammates --adapter codex Use a specific agent adapter
8
8
  * teammates --dir <path> Override .teammates/ location
9
9
  */
10
- import { exec as execCb, execSync, spawn } from "node:child_process";
10
+ import { exec as execCb, execSync, spawnSync } from "node:child_process";
11
11
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
13
13
  import { dirname, join, resolve } from "node:path";
14
14
  import { createInterface } from "node:readline";
15
- import { App, ChatView, concat, esc, Interview, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
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
18
  import { syncRecallIndex } from "./adapter.js";
@@ -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() {
@@ -103,10 +206,10 @@ class TeammatesREPL {
103
206
  : `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Summarize the conversation entries below.\n\n## Entries to Summarize\n\n${entriesText}\n\n## Instructions\n\nReturn ONLY the summary — no preamble, no explanation. The summary should:\n- Be a concise bulleted list of key topics discussed, decisions made, and work completed\n- Preserve important context that future messages might reference\n- Drop trivial or redundant details\n- Stay under 2000 characters\n- Do NOT include any output protocol (no TO:, no # Subject, no handoff blocks)`;
104
207
  // Remove the summarized entries — they'll be captured in the summary
105
208
  this.conversationHistory.splice(0, splitIdx);
106
- // Queue the summarization task to the base coding agent
209
+ // Queue the summarization task through the user's agent
107
210
  this.taskQueue.push({
108
211
  type: "summarize",
109
- teammate: this.adapterName,
212
+ teammate: this.selfName,
110
213
  task: prompt,
111
214
  });
112
215
  this.kickDrain();
@@ -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. */
@@ -142,9 +247,16 @@ class TeammatesREPL {
142
247
  _replyContexts = new Map();
143
248
  /** Quoted reply text to expand on next submit. */
144
249
  _pendingQuotedReply = null;
145
- defaultFooter = null; // cached default footer content
250
+ /** Resolver for inline ask when set, next submit resolves this instead of normal handling. */
251
+ _pendingAsk = null;
252
+ defaultFooter = null; // cached left footer content
253
+ defaultFooterRight = null; // cached right footer content
146
254
  /** Cached service statuses for banner + /configure. */
147
255
  serviceStatuses = [];
256
+ /** Reference to the animated banner widget for live updates. */
257
+ banner = null;
258
+ /** The local user's alias (avatar name). Set after USER.md is read or interview completes. */
259
+ userAlias = null;
148
260
  // ── Animated status tracker ─────────────────────────────────────
149
261
  activeTasks = new Map();
150
262
  statusTimer = null;
@@ -166,6 +278,13 @@ class TeammatesREPL {
166
278
  constructor(adapterName) {
167
279
  this.adapterName = adapterName;
168
280
  }
281
+ /**
282
+ * The name used for the local user in the roster.
283
+ * Returns the user's alias if set, otherwise the adapter name.
284
+ */
285
+ get selfName() {
286
+ return this.userAlias ?? this.adapterName;
287
+ }
169
288
  /** Show the prompt with the fenced border. */
170
289
  showPrompt() {
171
290
  if (this.chatView) {
@@ -221,27 +340,28 @@ class TeammatesREPL {
221
340
  const entries = Array.from(this.activeTasks.values());
222
341
  const idx = this.statusRotateIndex % entries.length;
223
342
  const { teammate, task } = entries[idx];
343
+ const displayName = teammate === this.selfName ? this.adapterName : teammate;
224
344
  const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
225
345
  const taskPreview = task.length > 50 ? `${task.slice(0, 47)}...` : task;
226
346
  const queueInfo = this.activeTasks.size > 1 ? ` (${idx + 1}/${this.activeTasks.size})` : "";
227
347
  if (this.chatView) {
228
348
  // Strip newlines and truncate task text for single-line display
229
349
  const cleanTask = task.replace(/[\r\n]+/g, " ").trim();
230
- const maxLen = Math.max(20, (process.stdout.columns || 80) - teammate.length - 10);
350
+ const maxLen = Math.max(20, (process.stdout.columns || 80) - displayName.length - 10);
231
351
  const taskText = cleanTask.length > maxLen
232
352
  ? `${cleanTask.slice(0, maxLen - 1)}…`
233
353
  : cleanTask;
234
354
  const queueTag = this.activeTasks.size > 1
235
355
  ? ` (${idx + 1}/${this.activeTasks.size})`
236
356
  : "";
237
- this.chatView.setProgress(concat(tp.accent(`${spinChar} ${teammate}… `), tp.muted(taskText + queueTag)));
357
+ this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}… `), tp.muted(taskText + queueTag)));
238
358
  this.app.refresh();
239
359
  }
240
360
  else {
241
361
  // Mostly bright blue, periodically flicker to dark blue
242
362
  const spinColor = this.statusFrame % 8 === 0 ? chalk.blue : chalk.blueBright;
243
363
  const line = ` ${spinColor(spinChar)} ` +
244
- chalk.bold(teammate) +
364
+ chalk.bold(displayName) +
245
365
  chalk.gray(`… ${taskPreview}`) +
246
366
  (queueInfo ? chalk.gray(queueInfo) : "");
247
367
  this.input.setStatus(line);
@@ -299,8 +419,8 @@ class TeammatesREPL {
299
419
  rendered.push({ type: "text", content: line });
300
420
  }
301
421
  }
302
- // Render first line with "User: " label
303
- const label = "user: ";
422
+ // Render first line with alias label
423
+ const label = `${this.selfName}: `;
304
424
  const first = rendered.shift();
305
425
  if (first) {
306
426
  if (first.type === "text") {
@@ -483,7 +603,7 @@ class TeammatesREPL {
483
603
  style: { fg: chrome },
484
604
  }));
485
605
  if (!isValid) {
486
- this.feedLine(tp.error(` ✖ Unknown teammate: @${h.to}`));
606
+ this.feedLine(tp.error(` ✖ Unknown teammate: @${h.to}`));
487
607
  }
488
608
  else if (this.autoApproveHandoffs) {
489
609
  this.taskQueue.push({ type: "agent", teammate: h.to, task: h.task });
@@ -847,13 +967,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
847
967
  if (this.app)
848
968
  this.app.refresh();
849
969
  }
850
- queueTask(input) {
970
+ queueTask(input, preMentions) {
851
971
  const allNames = this.orchestrator.listTeammates();
852
972
  // Check for @everyone — queue to all teammates except the coding agent
853
973
  const everyoneMatch = input.match(/^@everyone\s+([\s\S]+)$/i);
854
974
  if (everyoneMatch) {
855
975
  const task = everyoneMatch[1];
856
- const names = allNames.filter((n) => n !== this.adapterName);
976
+ const names = allNames.filter((n) => n !== this.selfName && n !== this.adapterName);
857
977
  for (const teammate of names) {
858
978
  this.taskQueue.push({ type: "agent", teammate, task });
859
979
  }
@@ -865,14 +985,22 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
865
985
  this.kickDrain();
866
986
  return;
867
987
  }
868
- // Collect all @mentioned teammates anywhere in the input
869
- const mentionRegex = /@(\S+)/g;
870
- let m;
871
- const mentioned = [];
872
- while ((m = mentionRegex.exec(input)) !== null) {
873
- const name = m[1];
874
- if (allNames.includes(name) && !mentioned.includes(name)) {
875
- mentioned.push(name);
988
+ // Use pre-resolved mentions if provided (avoids picking up @mentions from expanded paste text),
989
+ // otherwise scan the input directly.
990
+ let mentioned;
991
+ if (preMentions) {
992
+ mentioned = preMentions;
993
+ }
994
+ else {
995
+ const mentionRegex = /@(\S+)/g;
996
+ let m;
997
+ mentioned = [];
998
+ while ((m = mentionRegex.exec(input)) !== null) {
999
+ // Remap adapter name alias → user avatar for routing
1000
+ const name = (m[1] === this.adapterName && this.userAlias) ? this.selfName : m[1];
1001
+ if (allNames.includes(name) && !mentioned.includes(name)) {
1002
+ mentioned.push(name);
1003
+ }
876
1004
  }
877
1005
  }
878
1006
  if (mentioned.length > 0) {
@@ -894,12 +1022,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
894
1022
  match = this.lastResult.teammate;
895
1023
  }
896
1024
  if (!match) {
897
- match = this.orchestrator.route(input) ?? this.adapterName;
1025
+ match = this.orchestrator.route(input) ?? this.selfName;
898
1026
  }
899
1027
  {
900
1028
  const bg = this._userBg;
901
1029
  const t = theme();
902
- this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${match}`)));
1030
+ const displayName = match === this.selfName ? this.adapterName : match;
1031
+ this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${displayName}`)));
903
1032
  }
904
1033
  this.feedLine();
905
1034
  this.refreshView();
@@ -924,66 +1053,184 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
924
1053
  }
925
1054
  // ─── Onboarding ───────────────────────────────────────────────────
926
1055
  /**
927
- * Interactive prompt when no .teammates/ directory is found.
928
- * Returns the new .teammates/ path, or null if user chose to exit.
1056
+ * Interactive prompt for team onboarding after user profile is set up.
1057
+ * .teammates/ already exists at this point. Returns false if user chose to exit.
929
1058
  */
930
- async promptOnboarding(adapter) {
1059
+ async promptTeamOnboarding(adapter, teammatesDir) {
931
1060
  const cwd = process.cwd();
932
- const teammatesDir = join(cwd, ".teammates");
933
1061
  const termWidth = process.stdout.columns || 100;
934
1062
  console.log();
935
- this.printLogo([
936
- chalk.bold("Teammates") + chalk.gray(` v${PKG_VERSION}`),
937
- chalk.yellow("No .teammates/ directory found"),
938
- chalk.gray(cwd),
939
- ]);
940
- console.log();
941
1063
  console.log(chalk.gray("─".repeat(termWidth)));
942
1064
  console.log();
943
1065
  console.log(chalk.white(" Set up teammates for this project?\n"));
944
1066
  console.log(chalk.cyan(" 1") +
945
1067
  chalk.gray(") ") +
946
- chalk.white("New team") +
947
- chalk.gray(" — analyze this codebase and create teammates from scratch"));
1068
+ chalk.white("Pick teammates") +
1069
+ chalk.gray(" — choose from persona templates"));
948
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") +
949
1075
  chalk.gray(") ") +
950
1076
  chalk.white("Import team") +
951
1077
  chalk.gray(" — copy teammates from another project"));
952
- console.log(chalk.cyan(" 3") +
1078
+ console.log(chalk.cyan(" 4") +
953
1079
  chalk.gray(") ") +
954
1080
  chalk.white("Solo mode") +
955
- chalk.gray(` — use ${this.adapterName} without teammates`));
956
- console.log(chalk.cyan(" 4") + chalk.gray(") ") + chalk.white("Exit"));
1081
+ chalk.gray(" — use your agent without teammates"));
1082
+ console.log(chalk.cyan(" 5") + chalk.gray(") ") + chalk.white("Exit"));
957
1083
  console.log();
958
- 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): ", [
959
1085
  "1",
960
1086
  "2",
961
1087
  "3",
962
1088
  "4",
1089
+ "5",
963
1090
  ]);
964
- if (choice === "4") {
1091
+ if (choice === "5") {
965
1092
  console.log(chalk.gray(" Goodbye."));
966
- return null;
1093
+ return false;
967
1094
  }
968
- if (choice === "3") {
969
- await mkdir(teammatesDir, { recursive: true });
970
- console.log();
971
- console.log(chalk.green(" ✔") + chalk.gray(` Created ${teammatesDir}`));
972
- console.log(chalk.gray(` Running in solo mode — all tasks go to ${this.adapterName}.`));
1095
+ if (choice === "4") {
1096
+ console.log(chalk.gray(" Running in solo mode — all tasks go to your agent."));
973
1097
  console.log(chalk.gray(" Run /init later to set up teammates."));
974
1098
  console.log();
975
- return teammatesDir;
1099
+ return true;
976
1100
  }
977
- if (choice === "2") {
978
- // Import from another project
979
- await mkdir(teammatesDir, { recursive: true });
1101
+ if (choice === "3") {
980
1102
  await this.runImport(cwd);
981
- return teammatesDir;
1103
+ return true;
982
1104
  }
983
- // choice === "1": Run onboarding via the agent
984
- await mkdir(teammatesDir, { recursive: true });
985
- await this.runOnboardingAgent(adapter, cwd);
986
- return teammatesDir;
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);
1112
+ return true;
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();
987
1234
  }
988
1235
  /**
989
1236
  * Run the onboarding agent to analyze the codebase and create teammates.
@@ -992,19 +1239,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
992
1239
  async runOnboardingAgent(adapter, projectDir) {
993
1240
  console.log();
994
1241
  console.log(chalk.blue(" Starting onboarding...") +
995
- chalk.gray(` ${this.adapterName} will analyze your codebase and create .teammates/`));
1242
+ chalk.gray(" Your agent will analyze your codebase and create .teammates/"));
996
1243
  console.log();
997
1244
  // Copy framework files from bundled template
998
1245
  const teammatesDir = join(projectDir, ".teammates");
999
1246
  const copied = await copyTemplateFiles(teammatesDir);
1000
1247
  if (copied.length > 0) {
1001
- console.log(chalk.green(" ✔") +
1248
+ console.log(chalk.green(" ✔ ") +
1002
1249
  chalk.gray(` Copied template files: ${copied.join(", ")}`));
1003
1250
  console.log();
1004
1251
  }
1005
1252
  const onboardingPrompt = await getOnboardingPrompt(projectDir);
1006
1253
  const tempConfig = {
1007
1254
  name: this.adapterName,
1255
+ type: "ai",
1008
1256
  role: "Onboarding agent",
1009
1257
  soul: "",
1010
1258
  wisdom: "",
@@ -1016,8 +1264,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1016
1264
  };
1017
1265
  const sessionId = await adapter.startSession(tempConfig);
1018
1266
  const spinner = ora({
1019
- text: chalk.blue(this.adapterName) +
1020
- chalk.gray(" is analyzing your codebase..."),
1267
+ text: chalk.gray("Analyzing your codebase..."),
1021
1268
  spinner: "dots",
1022
1269
  }).start();
1023
1270
  try {
@@ -1025,7 +1272,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1025
1272
  spinner.stop();
1026
1273
  this.printAgentOutput(result.rawOutput);
1027
1274
  if (result.success) {
1028
- console.log(chalk.green(" ✔ Onboarding complete!"));
1275
+ console.log(chalk.green(" ✔ Onboarding complete!"));
1029
1276
  }
1030
1277
  else {
1031
1278
  console.log(chalk.yellow(` ⚠ Onboarding finished with issues: ${result.summary}`));
@@ -1091,7 +1338,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1091
1338
  return;
1092
1339
  }
1093
1340
  if (teammates.length > 0) {
1094
- console.log(chalk.green(" ✔") +
1341
+ console.log(chalk.green(" ✔ ") +
1095
1342
  chalk.white(` Imported ${teammates.length} teammate${teammates.length > 1 ? "s" : ""}: `) +
1096
1343
  chalk.cyan(teammates.join(", ")));
1097
1344
  console.log(chalk.gray(` (${files.length} files copied)`));
@@ -1131,11 +1378,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1131
1378
  const teammatesDir = join(projectDir, ".teammates");
1132
1379
  console.log();
1133
1380
  console.log(chalk.blue(" Starting adaptation...") +
1134
- chalk.gray(` ${this.adapterName} will scan this project and adapt the team`));
1381
+ chalk.gray(" Your agent will scan this project and adapt the team"));
1135
1382
  console.log();
1136
1383
  const prompt = await buildImportAdaptationPrompt(teammatesDir, teammateNames, sourceProjectPath);
1137
1384
  const tempConfig = {
1138
1385
  name: this.adapterName,
1386
+ type: "ai",
1139
1387
  role: "Adaptation agent",
1140
1388
  soul: "",
1141
1389
  wisdom: "",
@@ -1147,8 +1395,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1147
1395
  };
1148
1396
  const sessionId = await adapter.startSession(tempConfig);
1149
1397
  const spinner = ora({
1150
- text: chalk.blue(this.adapterName) +
1151
- chalk.gray(" is scanning the project and adapting teammates..."),
1398
+ text: chalk.gray("Scanning the project and adapting teammates..."),
1152
1399
  spinner: "dots",
1153
1400
  }).start();
1154
1401
  try {
@@ -1156,7 +1403,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1156
1403
  spinner.stop();
1157
1404
  this.printAgentOutput(result.rawOutput);
1158
1405
  if (result.success) {
1159
- console.log(chalk.green(" ✔ Team adaptation complete!"));
1406
+ console.log(chalk.green(" ✔ Team adaptation complete!"));
1160
1407
  }
1161
1408
  else {
1162
1409
  console.log(chalk.yellow(` ⚠ Adaptation finished with issues: ${result.summary}`));
@@ -1206,6 +1453,30 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1206
1453
  });
1207
1454
  });
1208
1455
  }
1456
+ /**
1457
+ * Ask for input using the ChatView's own prompt (no raw readline).
1458
+ * Temporarily replaces the footer with the prompt text and intercepts the next submit.
1459
+ */
1460
+ askInline(prompt) {
1461
+ return new Promise((resolve) => {
1462
+ if (!this.chatView) {
1463
+ // Fallback if no ChatView (shouldn't happen during /configure)
1464
+ return this.askInput(prompt).then(resolve);
1465
+ }
1466
+ // Show the prompt in the feed so it's visible
1467
+ this.feedLine(tp.accent(` ${prompt}`));
1468
+ this.chatView.setFooter(tp.accent(` ${prompt}`));
1469
+ this._pendingAsk = (answer) => {
1470
+ // Restore footer
1471
+ if (this.chatView && this.defaultFooter) {
1472
+ this.chatView.setFooter(this.defaultFooter);
1473
+ }
1474
+ this.refreshView();
1475
+ resolve(answer.trim());
1476
+ };
1477
+ this.refreshView();
1478
+ });
1479
+ }
1209
1480
  /**
1210
1481
  * Check whether USER.md needs to be created or is still template placeholders.
1211
1482
  */
@@ -1222,71 +1493,315 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1222
1493
  }
1223
1494
  }
1224
1495
  /**
1225
- * Run the user interview inside the ChatView using the Interview widget.
1226
- * Hides the normal input prompt until the interview completes.
1496
+ * Pre-TUI user profile setup. Runs in the console before the ChatView is created.
1497
+ * Offers GitHub-based or manual profile creation.
1227
1498
  */
1228
- startUserInterview(teammatesDir, bannerWidget) {
1229
- if (!this.chatView)
1499
+ async runUserSetup(teammatesDir) {
1500
+ const termWidth = process.stdout.columns || 100;
1501
+ console.log();
1502
+ console.log(chalk.gray("─".repeat(termWidth)));
1503
+ console.log();
1504
+ console.log(chalk.white(" Set up your profile\n"));
1505
+ console.log(chalk.cyan(" 1") +
1506
+ chalk.gray(") ") +
1507
+ chalk.white("Use GitHub account") +
1508
+ chalk.gray(" — import your name and username from GitHub"));
1509
+ console.log(chalk.cyan(" 2") +
1510
+ chalk.gray(") ") +
1511
+ chalk.white("Manual setup") +
1512
+ chalk.gray(" — enter your details manually"));
1513
+ console.log(chalk.cyan(" 3") +
1514
+ chalk.gray(") ") +
1515
+ chalk.white("Skip") +
1516
+ chalk.gray(" — set up later with /user"));
1517
+ console.log();
1518
+ const choice = await this.askChoice("Pick an option (1/2/3): ", [
1519
+ "1",
1520
+ "2",
1521
+ "3",
1522
+ ]);
1523
+ if (choice === "3") {
1524
+ console.log(chalk.gray(" Skipped — run /user to set up your profile later."));
1525
+ console.log();
1230
1526
  return;
1231
- const t = theme();
1232
- const interview = new Interview({
1233
- title: "Quick intro — helps teammates tailor their work to you.",
1234
- subtitle: "(press Enter to skip any question)",
1235
- questions: [
1236
- { key: "name", prompt: "Your name" },
1237
- {
1238
- key: "role",
1239
- prompt: "Your role",
1240
- placeholder: "e.g., senior backend engineer",
1241
- },
1242
- {
1243
- key: "experience",
1244
- prompt: "Relevant experience",
1245
- placeholder: "e.g., 10 years Go, new to React",
1246
- },
1247
- {
1248
- key: "preferences",
1249
- prompt: "How you like to work",
1250
- placeholder: "e.g., terse responses, explain reasoning",
1251
- },
1252
- {
1253
- key: "context",
1254
- prompt: "Anything else",
1255
- placeholder: "e.g., solo dev, working on a rewrite",
1256
- },
1257
- ],
1258
- titleStyle: { fg: t.text },
1259
- subtitleStyle: { fg: t.textDim, italic: true },
1260
- promptStyle: { fg: t.accent },
1261
- answeredStyle: { fg: t.textMuted, italic: true },
1262
- inputStyle: { fg: t.text },
1263
- cursorStyle: { fg: t.cursorFg, bg: t.cursorBg },
1264
- placeholderStyle: { fg: t.textDim, italic: true },
1265
- });
1266
- this.chatView.setInputOverride(interview);
1267
- if (this.app)
1268
- this.app.refresh();
1269
- interview.on("complete", (answers) => {
1270
- // Write USER.md
1271
- const userMdPath = join(teammatesDir, "USER.md");
1272
- const lines = ["# User\n"];
1273
- lines.push(`- **Name:** ${answers.name || "_not provided_"}`);
1274
- lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
1275
- lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
1276
- lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
1277
- lines.push(`- **Context:** ${answers.context || "_not provided_"}`);
1278
- writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
1279
- // Remove override and restore normal input
1280
- if (this.chatView) {
1281
- this.chatView.setInputOverride(null);
1282
- this.chatView.appendStyledToFeed(concat(tp.success(" "), tp.dim("Saved USER.md — update anytime with /user")));
1283
- }
1284
- // Release the banner hold so commands animate in
1285
- if (bannerWidget)
1286
- bannerWidget.releaseHold();
1287
- if (this.app)
1288
- this.app.refresh();
1527
+ }
1528
+ if (choice === "1") {
1529
+ await this.setupGitHubProfile(teammatesDir);
1530
+ }
1531
+ else {
1532
+ await this.setupManualProfile(teammatesDir);
1533
+ }
1534
+ }
1535
+ /**
1536
+ * GitHub-based profile setup. Ensures gh is installed and authenticated,
1537
+ * then fetches user info from the GitHub API to create the profile.
1538
+ */
1539
+ async setupGitHubProfile(teammatesDir) {
1540
+ console.log();
1541
+ // Step 1: Check if gh is installed
1542
+ let ghInstalled = false;
1543
+ try {
1544
+ execSync("gh --version", { stdio: "pipe" });
1545
+ ghInstalled = true;
1546
+ }
1547
+ catch {
1548
+ // not installed
1549
+ }
1550
+ if (!ghInstalled) {
1551
+ console.log(chalk.yellow(" GitHub CLI is not installed.\n"));
1552
+ const plat = process.platform;
1553
+ console.log(chalk.white(" Run this in another terminal:"));
1554
+ if (plat === "win32") {
1555
+ console.log(chalk.cyan(" winget install --id GitHub.cli"));
1556
+ }
1557
+ else if (plat === "darwin") {
1558
+ console.log(chalk.cyan(" brew install gh"));
1559
+ }
1560
+ else {
1561
+ console.log(chalk.cyan(" sudo apt install gh"));
1562
+ console.log(chalk.gray(" (or see https://cli.github.com)"));
1563
+ }
1564
+ console.log();
1565
+ const answer = await this.askChoice("Press Enter when done, or s to skip: ", ["", "s", "S"]);
1566
+ if (answer.toLowerCase() === "s") {
1567
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1568
+ return this.setupManualProfile(teammatesDir);
1569
+ }
1570
+ // Re-check
1571
+ try {
1572
+ execSync("gh --version", { stdio: "pipe" });
1573
+ ghInstalled = true;
1574
+ console.log(chalk.green(" ✔ GitHub CLI installed"));
1575
+ }
1576
+ catch {
1577
+ console.log(chalk.yellow(" GitHub CLI still not found. You may need to restart your terminal."));
1578
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1579
+ return this.setupManualProfile(teammatesDir);
1580
+ }
1581
+ }
1582
+ else {
1583
+ console.log(chalk.green(" ✔ GitHub CLI installed"));
1584
+ }
1585
+ // Step 2: Check auth
1586
+ let authed = false;
1587
+ try {
1588
+ execSync("gh auth status", { stdio: "pipe" });
1589
+ authed = true;
1590
+ }
1591
+ catch {
1592
+ // not authenticated
1593
+ }
1594
+ if (!authed) {
1595
+ console.log();
1596
+ console.log(chalk.gray(" Authenticating with GitHub...\n"));
1597
+ const result = spawnSync("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
1598
+ stdio: "inherit",
1599
+ shell: true,
1600
+ });
1601
+ if (result.status !== 0) {
1602
+ console.log(chalk.yellow(" Authentication failed or was cancelled."));
1603
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1604
+ return this.setupManualProfile(teammatesDir);
1605
+ }
1606
+ // Verify
1607
+ try {
1608
+ execSync("gh auth status", { stdio: "pipe" });
1609
+ authed = true;
1610
+ }
1611
+ catch {
1612
+ console.log(chalk.yellow(" Authentication could not be verified."));
1613
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1614
+ return this.setupManualProfile(teammatesDir);
1615
+ }
1616
+ }
1617
+ console.log(chalk.green(" ✔ GitHub authenticated"));
1618
+ // Step 3: Fetch user info from GitHub API
1619
+ let login = "";
1620
+ let name = "";
1621
+ try {
1622
+ const json = execSync("gh api user", {
1623
+ stdio: "pipe",
1624
+ encoding: "utf-8",
1625
+ });
1626
+ const user = JSON.parse(json);
1627
+ login = (user.login || "").toLowerCase().replace(/[^a-z0-9_-]/g, "");
1628
+ name = user.name || user.login || "";
1629
+ }
1630
+ catch {
1631
+ console.log(chalk.yellow(" Could not fetch GitHub user info."));
1632
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1633
+ return this.setupManualProfile(teammatesDir);
1634
+ }
1635
+ if (!login) {
1636
+ console.log(chalk.yellow(" No GitHub username found."));
1637
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1638
+ return this.setupManualProfile(teammatesDir);
1639
+ }
1640
+ console.log(chalk.green(` ✔ Authenticated as `) +
1641
+ chalk.cyan(`@${login}`) +
1642
+ (name && name !== login ? chalk.gray(` (${name})`) : ""));
1643
+ console.log();
1644
+ // Ask for remaining fields since GitHub doesn't provide them
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}]` : ""}: `);
1651
+ const answers = {
1652
+ alias: login,
1653
+ name: name || login,
1654
+ role: role || "",
1655
+ experience: experience || "",
1656
+ preferences: preferences || "",
1657
+ timezone: timezone || detectedTz || "",
1658
+ };
1659
+ this.writeUserProfile(teammatesDir, login, answers);
1660
+ this.createUserAvatar(teammatesDir, login, answers);
1661
+ console.log(chalk.green(" ✔ ") +
1662
+ chalk.gray(`Profile created — avatar @${login}`));
1663
+ console.log();
1664
+ }
1665
+ /**
1666
+ * Manual (console-based) profile setup. Collects fields via askInput().
1667
+ */
1668
+ async setupManualProfile(teammatesDir) {
1669
+ console.log();
1670
+ console.log(chalk.gray(" (alias is required, press Enter to skip others)\n"));
1671
+ const aliasRaw = await this.askInput("Your alias (e.g., alex): ");
1672
+ const alias = aliasRaw.toLowerCase().replace(/[^a-z0-9_-]/g, "").trim();
1673
+ if (!alias) {
1674
+ console.log(chalk.yellow(" Alias is required. Run /user to try again.\n"));
1675
+ return;
1676
+ }
1677
+ const name = await this.askInput("Your name: ");
1678
+ const role = await this.askInput("Your role (e.g., senior backend engineer): ");
1679
+ const experience = await this.askInput("Relevant experience (e.g., 10 years Go, new to React): ");
1680
+ const preferences = await this.askInput("How you like to work (e.g., terse responses): ");
1681
+ // Auto-detect timezone
1682
+ const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1683
+ const timezone = await this.askInput(`Primary timezone${detectedTz ? ` [${detectedTz}]` : ""}: `);
1684
+ const answers = {
1685
+ alias,
1686
+ name,
1687
+ role,
1688
+ experience,
1689
+ preferences,
1690
+ timezone: timezone || detectedTz || "",
1691
+ };
1692
+ this.writeUserProfile(teammatesDir, alias, answers);
1693
+ this.createUserAvatar(teammatesDir, alias, answers);
1694
+ console.log();
1695
+ console.log(chalk.green(" ✔ ") +
1696
+ chalk.gray(`Profile created — avatar @${alias}`));
1697
+ console.log(chalk.gray(" Update anytime with /user"));
1698
+ console.log();
1699
+ }
1700
+ /**
1701
+ * Write USER.md from collected answers.
1702
+ */
1703
+ writeUserProfile(teammatesDir, alias, answers) {
1704
+ const userMdPath = join(teammatesDir, "USER.md");
1705
+ const lines = ["# User\n"];
1706
+ lines.push(`- **Alias:** ${alias}`);
1707
+ lines.push(`- **Name:** ${answers.name || "_not provided_"}`);
1708
+ lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
1709
+ lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
1710
+ lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
1711
+ lines.push(`- **Primary Timezone:** ${answers.timezone || "_not provided_"}`);
1712
+ writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
1713
+ }
1714
+ /**
1715
+ * Create the user's avatar folder with SOUL.md and WISDOM.md.
1716
+ * The avatar is a teammate folder with type: human.
1717
+ */
1718
+ createUserAvatar(teammatesDir, alias, answers) {
1719
+ const avatarDir = join(teammatesDir, alias);
1720
+ const memoryDir = join(avatarDir, "memory");
1721
+ mkdirSync(avatarDir, { recursive: true });
1722
+ mkdirSync(memoryDir, { recursive: true });
1723
+ const name = answers.name || alias;
1724
+ const role = answers.role || "I'm a human working on this project";
1725
+ const experience = answers.experience || "";
1726
+ const preferences = answers.preferences || "";
1727
+ const timezone = answers.timezone || "";
1728
+ // Write SOUL.md
1729
+ const soulLines = [
1730
+ `# ${name}`,
1731
+ "",
1732
+ "## Identity",
1733
+ "",
1734
+ `**Type:** human`,
1735
+ `**Alias:** ${alias}`,
1736
+ `**Role:** ${role}`,
1737
+ ];
1738
+ if (experience)
1739
+ soulLines.push(`**Experience:** ${experience}`);
1740
+ if (preferences)
1741
+ soulLines.push(`**Preferences:** ${preferences}`);
1742
+ if (timezone)
1743
+ soulLines.push(`**Primary Timezone:** ${timezone}`);
1744
+ soulLines.push("");
1745
+ const soulPath = join(avatarDir, "SOUL.md");
1746
+ writeFileSync(soulPath, soulLines.join("\n"), "utf-8");
1747
+ // Write empty WISDOM.md
1748
+ const wisdomPath = join(avatarDir, "WISDOM.md");
1749
+ writeFileSync(wisdomPath, `# ${name} — Wisdom\n\nDistilled from work history. Updated during compaction.\n`, "utf-8");
1750
+ // Avatar registration happens later in start() after the orchestrator is initialized.
1751
+ // During pre-TUI setup, the orchestrator doesn't exist yet.
1752
+ }
1753
+ /**
1754
+ * Read USER.md and extract the alias field.
1755
+ * Returns null if USER.md doesn't exist or has no alias.
1756
+ */
1757
+ readUserAlias(teammatesDir) {
1758
+ try {
1759
+ const content = readFileSync(join(teammatesDir, "USER.md"), "utf-8");
1760
+ const match = content.match(/\*\*Alias:\*\*\s*(\S+)/);
1761
+ return match ? match[1].toLowerCase().replace(/[^a-z0-9_-]/g, "") : null;
1762
+ }
1763
+ catch {
1764
+ return null;
1765
+ }
1766
+ }
1767
+ /**
1768
+ * Register the user's avatar as a teammate in the orchestrator.
1769
+ * Sets presence to "online" since the local user is always online.
1770
+ * Replaces the old coding agent entry.
1771
+ */
1772
+ registerUserAvatar(teammatesDir, alias) {
1773
+ const registry = this.orchestrator.getRegistry();
1774
+ const avatarDir = join(teammatesDir, alias);
1775
+ // Read the avatar's SOUL.md if it exists
1776
+ let soul = "";
1777
+ let role = "I'm a human working on this project";
1778
+ try {
1779
+ soul = readFileSync(join(avatarDir, "SOUL.md"), "utf-8");
1780
+ const roleMatch = soul.match(/\*\*Role:\*\*\s*(.+)/);
1781
+ if (roleMatch)
1782
+ role = roleMatch[1].trim();
1783
+ }
1784
+ catch { /* avatar folder may not exist yet */ }
1785
+ let wisdom = "";
1786
+ try {
1787
+ wisdom = readFileSync(join(avatarDir, "WISDOM.md"), "utf-8");
1788
+ }
1789
+ catch { /* ok */ }
1790
+ registry.register({
1791
+ name: alias,
1792
+ type: "human",
1793
+ role,
1794
+ soul,
1795
+ wisdom,
1796
+ dailyLogs: [],
1797
+ weeklyLogs: [],
1798
+ ownership: { primary: [], secondary: [] },
1799
+ routingKeywords: [],
1289
1800
  });
1801
+ // Set presence to online (local user is always online)
1802
+ this.orchestrator.getAllStatuses().set(alias, { state: "idle", presence: "online" });
1803
+ // Update the adapter name so tasks route to the avatar
1804
+ this.userAlias = alias;
1290
1805
  }
1291
1806
  // ─── Display helpers ──────────────────────────────────────────────
1292
1807
  /**
@@ -1472,12 +1987,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1472
1987
  });
1473
1988
  }
1474
1989
  for (const name of teammates) {
1475
- 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)) {
1476
1993
  const t = this.orchestrator.getRegistry().get(name);
1477
1994
  items.push({
1478
- label: `@${name}`,
1995
+ label: `@${display}`,
1479
1996
  description: t?.role ?? "",
1480
- completion: `${before}@${name} ${after.replace(/^\s+/, "")}`,
1997
+ completion: `${before}@${display} ${after.replace(/^\s+/, "")}`,
1481
1998
  });
1482
1999
  }
1483
2000
  }
@@ -1613,15 +2130,29 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1613
2130
  agentPassthrough: cliArgs.agentPassthrough,
1614
2131
  });
1615
2132
  this.adapter = adapter;
1616
- // No .teammates/ found offer onboarding or solo mode
2133
+ // Detect whether this is a brand-new project (no .teammates/ at all)
2134
+ const isNewProject = !teammatesDir;
1617
2135
  if (!teammatesDir) {
1618
- teammatesDir = await this.promptOnboarding(adapter);
1619
- if (!teammatesDir)
2136
+ teammatesDir = join(process.cwd(), ".teammates");
2137
+ await mkdir(teammatesDir, { recursive: true });
2138
+ // Show welcome logo for new projects
2139
+ console.log();
2140
+ this.printLogo([
2141
+ chalk.bold("Teammates") + chalk.gray(` v${PKG_VERSION}`),
2142
+ chalk.yellow("New project setup"),
2143
+ chalk.gray(process.cwd()),
2144
+ ]);
2145
+ }
2146
+ // Always onboard the user first if USER.md is missing
2147
+ if (this.needsUserSetup(teammatesDir)) {
2148
+ await this.runUserSetup(teammatesDir);
2149
+ }
2150
+ // Team onboarding if .teammates/ was missing
2151
+ if (isNewProject) {
2152
+ const cont = await this.promptTeamOnboarding(adapter, teammatesDir);
2153
+ if (!cont)
1620
2154
  return; // user chose to exit
1621
2155
  }
1622
- // Check if USER.md needs setup — we'll run the interview inside the
1623
- // ChatView after the UI loads (not before).
1624
- const pendingUserInterview = this.needsUserSetup(teammatesDir);
1625
2156
  // Init orchestrator
1626
2157
  this.teammatesDir = teammatesDir;
1627
2158
  this.orchestrator = new Orchestrator({
@@ -1630,26 +2161,38 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1630
2161
  onEvent: (e) => this.handleEvent(e),
1631
2162
  });
1632
2163
  await this.orchestrator.init();
1633
- // Register the agent itself as a mentionable teammate
1634
- const registry = this.orchestrator.getRegistry();
1635
- registry.register({
1636
- name: this.adapterName,
1637
- role: `General-purpose coding agent (${this.adapterName})`,
1638
- soul: "",
1639
- wisdom: "",
1640
- dailyLogs: [],
1641
- weeklyLogs: [],
1642
- ownership: { primary: [], secondary: [] },
1643
- routingKeywords: [],
1644
- cwd: dirname(this.teammatesDir),
1645
- });
1646
- // Add status entry (init() already ran, so we add it manually)
1647
- this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle" });
2164
+ // Register the local user's avatar if alias is configured.
2165
+ // The user's avatar is the entry point for all generic/fallback tasks —
2166
+ // the coding agent is an internal execution engine, not an addressable teammate.
2167
+ const alias = this.readUserAlias(teammatesDir);
2168
+ if (alias) {
2169
+ this.registerUserAvatar(teammatesDir, alias);
2170
+ }
2171
+ else {
2172
+ // No alias yet (solo mode or pre-interview). Register a minimal avatar
2173
+ // under the adapter name so internal tasks (btw, summarize, debug) can execute.
2174
+ const registry = this.orchestrator.getRegistry();
2175
+ registry.register({
2176
+ name: this.adapterName,
2177
+ type: "ai",
2178
+ role: "Coding agent that performs tasks on your behalf.",
2179
+ soul: "",
2180
+ wisdom: "",
2181
+ dailyLogs: [],
2182
+ weeklyLogs: [],
2183
+ ownership: { primary: [], secondary: [] },
2184
+ routingKeywords: [],
2185
+ cwd: dirname(this.teammatesDir),
2186
+ });
2187
+ this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle", presence: "online" });
2188
+ }
1648
2189
  // Populate roster on the adapter so prompts include team info
2190
+ // Exclude the user avatar and adapter fallback — neither is an addressable teammate
1649
2191
  if ("roster" in this.adapter) {
1650
2192
  const registry = this.orchestrator.getRegistry();
1651
2193
  this.adapter.roster = this.orchestrator
1652
2194
  .listTeammates()
2195
+ .filter((n) => n !== this.adapterName && n !== this.userAlias)
1653
2196
  .map((name) => {
1654
2197
  const t = registry.get(name);
1655
2198
  return { name: t.name, role: t.role, ownership: t.ownership };
@@ -1667,7 +2210,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1667
2210
  borderStyle: (s) => chalk.gray(s),
1668
2211
  colorize: (value) => {
1669
2212
  const validNames = new Set([
1670
- ...this.orchestrator.listTeammates(),
2213
+ ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
1671
2214
  this.adapterName,
1672
2215
  "everyone",
1673
2216
  ]);
@@ -1707,18 +2250,31 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1707
2250
  // ── Detect service statuses ────────────────────────────────────────
1708
2251
  this.serviceStatuses = this.detectServices();
1709
2252
  // ── Build animated banner for ChatView ─────────────────────────────
1710
- const names = this.orchestrator.listTeammates();
2253
+ const names = this.orchestrator
2254
+ .listTeammates()
2255
+ .filter((n) => n !== this.adapterName && n !== this.userAlias);
1711
2256
  const reg = this.orchestrator.getRegistry();
2257
+ const statuses = this.orchestrator.getAllStatuses();
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
+ }
2265
+ for (const name of names) {
2266
+ const t = reg.get(name);
2267
+ const p = statuses.get(name)?.presence ?? "online";
2268
+ bannerTeammates.push({ name, role: t?.role ?? "", presence: p });
2269
+ }
1712
2270
  const bannerWidget = new AnimatedBanner({
1713
- adapterName: this.adapterName,
2271
+ displayName: `@${this.adapterName}`,
1714
2272
  teammateCount: names.length,
1715
2273
  cwd: process.cwd(),
1716
- teammates: names.map((name) => {
1717
- const t = reg.get(name);
1718
- return { name, role: t?.role ?? "" };
1719
- }),
2274
+ teammates: bannerTeammates,
1720
2275
  services: this.serviceStatuses,
1721
2276
  });
2277
+ this.banner = bannerWidget;
1722
2278
  // ── Create ChatView and Consolonia App ────────────────────────────
1723
2279
  const t = theme();
1724
2280
  this.chatView = new ChatView({
@@ -1741,9 +2297,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1741
2297
  styles[i] = accentStyle;
1742
2298
  }
1743
2299
  }
1744
- // Colorize @mentions only if they reference a valid teammate or the coding agent
2300
+ // Colorize @mentions only if they reference a valid teammate or the user
1745
2301
  const validNames = new Set([
1746
- ...this.orchestrator.listTeammates(),
2302
+ ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName && n !== this.userAlias),
1747
2303
  this.adapterName,
1748
2304
  "everyone",
1749
2305
  ]);
@@ -1787,10 +2343,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1787
2343
  progressStyle: { fg: t.progress, italic: true },
1788
2344
  dropdownHighlightStyle: { fg: t.accent },
1789
2345
  dropdownStyle: { fg: t.textMuted },
1790
- footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`)),
2346
+ footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName)),
2347
+ footerRight: tp.muted("? /help "),
1791
2348
  footerStyle: { fg: t.textDim },
1792
2349
  });
1793
- this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`));
2350
+ this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName));
2351
+ this.defaultFooterRight = tp.muted("? /help ");
1794
2352
  // Wire ChatView events for input handling
1795
2353
  this.chatView.on("submit", (rawLine) => {
1796
2354
  this.handleSubmit(rawLine).catch((err) => {
@@ -1816,6 +2374,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1816
2374
  this.escTimer = null;
1817
2375
  }
1818
2376
  this.chatView.setFooter(this.defaultFooter);
2377
+ this.chatView.setFooterRight(this.defaultFooterRight);
1819
2378
  this.refreshView();
1820
2379
  }
1821
2380
  if (this.ctrlcPending) {
@@ -1825,6 +2384,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1825
2384
  this.ctrlcTimer = null;
1826
2385
  }
1827
2386
  this.chatView.setFooter(this.defaultFooter);
2387
+ this.chatView.setFooterRight(this.defaultFooterRight);
1828
2388
  this.refreshView();
1829
2389
  }
1830
2390
  });
@@ -1848,22 +2408,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1848
2408
  }
1849
2409
  this.chatView.inputValue = "";
1850
2410
  this.chatView.setFooter(this.defaultFooter);
2411
+ this.chatView.setFooterRight(this.defaultFooterRight);
1851
2412
  this.pastedTexts.clear();
1852
2413
  this.refreshView();
1853
2414
  }
1854
2415
  else if (this.chatView.inputValue.length > 0) {
1855
- // First ESC with text — show hint in footer, auto-expire after 2s
2416
+ // First ESC with text — show hint in footer right, auto-expire after 2s
1856
2417
  this.escPending = true;
1857
- const termW = process.stdout.columns || 80;
1858
- const hint = "ESC again to clear";
1859
- const pad = Math.max(0, termW - hint.length - 1);
1860
- this.chatView.setFooter(concat(tp.dim(" ".repeat(pad)), tp.muted(hint)));
2418
+ this.chatView.setFooterRight(tp.muted("ESC again to clear "));
1861
2419
  this.refreshView();
1862
2420
  this.escTimer = setTimeout(() => {
1863
2421
  this.escTimer = null;
1864
2422
  if (this.escPending) {
1865
2423
  this.escPending = false;
1866
2424
  this.chatView.setFooter(this.defaultFooter);
2425
+ this.chatView.setFooterRight(this.defaultFooterRight);
1867
2426
  this.refreshView();
1868
2427
  }
1869
2428
  }, 2000);
@@ -1881,29 +2440,31 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1881
2440
  this.ctrlcTimer = null;
1882
2441
  }
1883
2442
  this.chatView.setFooter(this.defaultFooter);
2443
+ this.chatView.setFooterRight(this.defaultFooterRight);
1884
2444
  if (this.app)
1885
2445
  this.app.stop();
1886
2446
  this.orchestrator.shutdown().then(() => process.exit(0));
1887
2447
  return;
1888
2448
  }
1889
- // First Ctrl+C — show hint in footer, auto-expire after 2s
2449
+ // First Ctrl+C — show hint in footer right, auto-expire after 2s
1890
2450
  this.ctrlcPending = true;
1891
- const termW = process.stdout.columns || 80;
1892
- const hint = "Ctrl+C again to exit";
1893
- const pad = Math.max(0, termW - hint.length - 1);
1894
- this.chatView.setFooter(concat(tp.dim(" ".repeat(pad)), tp.muted(hint)));
2451
+ this.chatView.setFooterRight(tp.muted("Ctrl+C again to exit "));
1895
2452
  this.refreshView();
1896
2453
  this.ctrlcTimer = setTimeout(() => {
1897
2454
  this.ctrlcTimer = null;
1898
2455
  if (this.ctrlcPending) {
1899
2456
  this.ctrlcPending = false;
1900
2457
  this.chatView.setFooter(this.defaultFooter);
2458
+ this.chatView.setFooterRight(this.defaultFooterRight);
1901
2459
  this.refreshView();
1902
2460
  }
1903
2461
  }, 2000);
1904
2462
  });
1905
2463
  this.chatView.on("action", (id) => {
1906
- if (id === "copy") {
2464
+ if (id.startsWith("copy-cmd:")) {
2465
+ this.doCopy(id.slice("copy-cmd:".length));
2466
+ }
2467
+ else if (id === "copy") {
1907
2468
  this.doCopy(this.lastCleanedOutput || undefined);
1908
2469
  }
1909
2470
  else if (id.startsWith("retro-approve-") ||
@@ -1952,15 +2513,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1952
2513
  // Start the banner animation after the first frame renders.
1953
2514
  bannerWidget.onDirty = () => this.app?.refresh();
1954
2515
  const runPromise = this.app.run();
1955
- // Hold the banner animation before commands if we need to run the interview
1956
- if (pendingUserInterview) {
1957
- bannerWidget.hold();
1958
- }
1959
2516
  bannerWidget.start();
1960
- // Run user interview inside the ChatView if USER.md needs setup
1961
- if (pendingUserInterview) {
1962
- this.startUserInterview(teammatesDir, bannerWidget);
1963
- }
1964
2517
  await runPromise;
1965
2518
  }
1966
2519
  /**
@@ -1983,7 +2536,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1983
2536
  const fileName = trimmed.split(/[/\\]/).pop() || trimmed;
1984
2537
  const n = ++this.pasteCounter;
1985
2538
  this.pastedTexts.set(n, `[Image: source: ${trimmed}]`);
1986
- const placeholder = `[Image ${fileName}]`;
2539
+ const placeholder = `[Image ${fileName}] `;
1987
2540
  const newVal = current.slice(0, idx) +
1988
2541
  placeholder +
1989
2542
  current.slice(idx + clean.length);
@@ -2020,9 +2573,29 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2020
2573
  }
2021
2574
  /** Handle line submission from ChatView. */
2022
2575
  async handleSubmit(rawLine) {
2576
+ // If an inline ask is pending, resolve it instead of normal processing
2577
+ if (this._pendingAsk) {
2578
+ const resolve = this._pendingAsk;
2579
+ this._pendingAsk = null;
2580
+ resolve(rawLine);
2581
+ return;
2582
+ }
2023
2583
  this.clearWordwheel();
2024
2584
  this.wordwheelItems = [];
2025
2585
  this.wordwheelIndex = -1;
2586
+ // Resolve @mentions from the raw input BEFORE paste expansion.
2587
+ // This prevents @mentions inside pasted/expanded text from being picked up.
2588
+ const allNames = this.orchestrator.listTeammates();
2589
+ const preMentionRegex = /@(\S+)/g;
2590
+ let pm;
2591
+ const preMentions = [];
2592
+ while ((pm = preMentionRegex.exec(rawLine)) !== null) {
2593
+ // Remap adapter name alias → user avatar for routing
2594
+ const name = (pm[1] === this.adapterName && this.userAlias) ? this.selfName : pm[1];
2595
+ if (allNames.includes(name) && !preMentions.includes(name)) {
2596
+ preMentions.push(name);
2597
+ }
2598
+ }
2026
2599
  // Expand paste placeholders with actual content
2027
2600
  let input = rawLine.replace(/\[Pasted text #(\d+) \+\d+ lines, [\d.]+KB\]\s*/g, (_match, num) => {
2028
2601
  const n = parseInt(num, 10);
@@ -2103,10 +2676,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2103
2676
  this.refreshView();
2104
2677
  return;
2105
2678
  }
2106
- // Everything else gets queued
2107
- this.conversationHistory.push({ role: "user", text: input });
2679
+ // Everything else gets queued.
2680
+ // Pass pre-resolved mentions so @mentions inside expanded paste text are ignored.
2681
+ this.conversationHistory.push({ role: this.selfName, text: input });
2108
2682
  this.printUserMessage(input);
2109
- this.queueTask(input);
2683
+ this.queueTask(input, preMentions);
2110
2684
  this.refreshView();
2111
2685
  }
2112
2686
  printBanner(teammates) {
@@ -2114,7 +2688,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2114
2688
  const termWidth = process.stdout.columns || 100;
2115
2689
  this.feedLine();
2116
2690
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2117
- this.feedLine(concat(tp.text(` ${this.adapterName}`), 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"}`)));
2118
2692
  this.feedLine(` ${process.cwd()}`);
2119
2693
  // Service status rows
2120
2694
  for (const svc of this.serviceStatuses) {
@@ -2130,12 +2704,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2130
2704
  : `missing — /configure ${svc.name.toLowerCase()}`;
2131
2705
  this.feedLine(concat(tp.text(" "), color(icon), color(svc.name), tp.muted(` ${label}`)));
2132
2706
  }
2133
- // Roster
2707
+ // Roster (with presence indicators)
2134
2708
  this.feedLine();
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
+ }
2135
2716
  for (const name of teammates) {
2136
2717
  const t = registry.get(name);
2137
2718
  if (t) {
2138
- this.feedLine(concat(tp.muted(" "), tp.accent(`● @${name.padEnd(14)}`), tp.muted(t.role)));
2719
+ const p = statuses.get(name)?.presence ?? "online";
2720
+ const dot = p === "online" ? tp.success("●") : p === "reachable" ? tp.warning("●") : tp.error("●");
2721
+ this.feedLine(concat(tp.text(" "), dot, tp.accent(` @${name.padEnd(14)}`), tp.muted(t.role)));
2139
2722
  }
2140
2723
  }
2141
2724
  this.feedLine();
@@ -2255,43 +2838,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2255
2838
  this.feedLine(tp.warning(" GitHub CLI is not installed."));
2256
2839
  this.feedLine();
2257
2840
  const plat = process.platform;
2258
- let installCmd;
2259
- let installLabel;
2841
+ this.feedLine(tp.text(" Run this in another terminal:"));
2260
2842
  if (plat === "win32") {
2261
- installCmd = "winget install --id GitHub.cli";
2262
- installLabel = "winget install --id GitHub.cli";
2843
+ this.feedCommand("winget install --id GitHub.cli");
2263
2844
  }
2264
2845
  else if (plat === "darwin") {
2265
- installCmd = "brew install gh";
2266
- installLabel = "brew install gh";
2846
+ this.feedCommand("brew install gh");
2267
2847
  }
2268
2848
  else {
2269
- installCmd = "sudo apt install gh";
2270
- installLabel = "sudo apt install gh (or see https://cli.github.com)";
2849
+ this.feedCommand("sudo apt install gh");
2850
+ this.feedLine(tp.muted(" (or see https://cli.github.com)"));
2271
2851
  }
2272
- this.feedLine(tp.text(` Install: ${installLabel}`));
2273
2852
  this.feedLine();
2274
- // Ask user
2275
- const answer = await this.askInput("Run install command? [Y/n] ");
2853
+ const answer = await this.askInline("Press Enter when done (or n to skip)");
2276
2854
  if (answer.toLowerCase() === "n") {
2277
- this.feedLine(tp.muted(" Skipped. Install manually and re-run /configure github"));
2278
- this.refreshView();
2279
- return;
2280
- }
2281
- // Spawn install in a visible subprocess
2282
- this.feedLine(tp.muted(` Running: ${installCmd}`));
2283
- this.refreshView();
2284
- const installSuccess = await new Promise((res) => {
2285
- const parts = installCmd.split(" ");
2286
- const child = spawn(parts[0], parts.slice(1), {
2287
- stdio: "inherit",
2288
- shell: true,
2289
- });
2290
- child.on("error", () => res(false));
2291
- child.on("exit", (code) => res(code === 0));
2292
- });
2293
- if (!installSuccess) {
2294
- this.feedLine(tp.error(" Install failed. Please install manually from https://cli.github.com"));
2855
+ this.feedLine(tp.muted(" Skipped. Run /configure github when ready."));
2295
2856
  this.refreshView();
2296
2857
  return;
2297
2858
  }
@@ -2302,7 +2863,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2302
2863
  this.feedLine(tp.success(" ✓ GitHub CLI installed"));
2303
2864
  }
2304
2865
  catch {
2305
- this.feedLine(tp.error(" GitHub CLI still not found after install. You may need to restart your terminal."));
2866
+ this.feedLine(tp.error(" GitHub CLI still not found. You may need to restart your terminal."));
2306
2867
  this.refreshView();
2307
2868
  return;
2308
2869
  }
@@ -2321,31 +2882,19 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2321
2882
  // not authenticated
2322
2883
  }
2323
2884
  if (!authed) {
2324
- this.feedLine(tp.muted(" Authentication needed — this will open your browser for GitHub OAuth."));
2325
2885
  this.feedLine();
2326
- const answer = await this.askInput("Start authentication? [Y/n] ");
2886
+ this.feedLine(tp.text(" Run this in another terminal to authenticate:"));
2887
+ this.feedCommand("gh auth login --web --git-protocol https");
2888
+ this.feedLine();
2889
+ this.feedLine(tp.muted(" This will open your browser for GitHub OAuth."));
2890
+ this.feedLine();
2891
+ const answer = await this.askInline("Press Enter when done (or n to skip)");
2327
2892
  if (answer.toLowerCase() === "n") {
2328
2893
  this.feedLine(tp.muted(" Skipped. Run /configure github when ready."));
2329
2894
  this.refreshView();
2330
2895
  this.updateServiceStatus("GitHub", "not-configured");
2331
2896
  return;
2332
2897
  }
2333
- this.feedLine(tp.muted(" Starting auth flow..."));
2334
- this.refreshView();
2335
- const authSuccess = await new Promise((res) => {
2336
- const child = spawn("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
2337
- stdio: "inherit",
2338
- shell: true,
2339
- });
2340
- child.on("error", () => res(false));
2341
- child.on("exit", (code) => res(code === 0));
2342
- });
2343
- if (!authSuccess) {
2344
- this.feedLine(tp.error(" Authentication failed. Try again with /configure github"));
2345
- this.refreshView();
2346
- this.updateServiceStatus("GitHub", "not-configured");
2347
- return;
2348
- }
2349
2898
  // Verify
2350
2899
  try {
2351
2900
  execSync("gh auth status", { stdio: "pipe" });
@@ -2373,8 +2922,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2373
2922
  }
2374
2923
  updateServiceStatus(name, status) {
2375
2924
  const svc = this.serviceStatuses.find((s) => s.name === name);
2376
- if (svc)
2925
+ if (svc) {
2377
2926
  svc.status = status;
2927
+ if (this.banner) {
2928
+ this.banner.updateServices(this.serviceStatuses);
2929
+ this.refreshView();
2930
+ }
2931
+ }
2378
2932
  }
2379
2933
  registerCommands() {
2380
2934
  const cmds = [
@@ -2409,8 +2963,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2409
2963
  {
2410
2964
  name: "init",
2411
2965
  aliases: ["onboard", "setup"],
2412
- usage: "/init [from-path]",
2413
- 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)",
2414
2968
  run: (args) => this.cmdInit(args),
2415
2969
  },
2416
2970
  {
@@ -2506,6 +3060,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2506
3060
  }
2507
3061
  // ─── Event handler ───────────────────────────────────────────────
2508
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;
2509
3071
  switch (event.type) {
2510
3072
  case "task_assigned": {
2511
3073
  // Track this task and start the animated status bar
@@ -2518,91 +3080,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2518
3080
  break;
2519
3081
  }
2520
3082
  case "task_completed": {
2521
- // 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.
2522
3086
  this.activeTasks.delete(event.result.teammate);
2523
3087
  // Stop animation if no more active tasks
2524
3088
  if (this.activeTasks.size === 0) {
2525
3089
  this.stopStatusAnimation();
2526
3090
  }
2527
- // Suppress display for internal summarization tasks
2528
- const activeEntry = this.agentActive.get(event.result.teammate);
2529
- if (activeEntry?.type === "summarize")
2530
- break;
2531
- if (!this.chatView)
2532
- this.input.deactivateAndErase();
2533
- const raw = event.result.rawOutput ?? "";
2534
- // Strip protocol artifacts
2535
- const cleaned = raw
2536
- .replace(/^TO:\s*\S+\s*\n/im, "")
2537
- .replace(/^#\s+.+\n*/m, "")
2538
- .replace(/```handoff\s*\n@\w+\s*\n[\s\S]*?```/g, "")
2539
- .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
2540
- .trim();
2541
- const sizeKB = cleaned ? Buffer.byteLength(cleaned, "utf-8") / 1024 : 0;
2542
- // Header: "teammate: subject"
2543
- const subject = event.result.summary || "Task completed";
2544
- this.feedLine(concat(tp.accent(`${event.result.teammate}: `), tp.text(subject)));
2545
- this.lastCleanedOutput = cleaned;
2546
- if (cleaned) {
2547
- this.feedMarkdown(cleaned);
2548
- }
2549
- else {
2550
- this.feedLine(tp.muted(" (no response text — the agent may have only performed tool actions)"));
2551
- this.feedLine(tp.muted(` Use /debug ${event.result.teammate} to view full output`));
2552
- // Show diagnostic hints for empty responses
2553
- const diag = event.result.diagnostics;
2554
- if (diag) {
2555
- if (diag.exitCode !== 0 && diag.exitCode !== null) {
2556
- this.feedLine(tp.warning(` ⚠ Process exited with code ${diag.exitCode}`));
2557
- }
2558
- if (diag.signal) {
2559
- this.feedLine(tp.warning(` ⚠ Process killed by signal: ${diag.signal}`));
2560
- }
2561
- if (diag.debugFile) {
2562
- this.feedLine(tp.muted(` Debug log: ${diag.debugFile}`));
2563
- }
2564
- }
2565
- }
2566
- // Render handoffs
2567
- const handoffs = event.result.handoffs;
2568
- if (handoffs.length > 0) {
2569
- this.renderHandoffs(event.result.teammate, handoffs);
2570
- }
2571
- // Clickable [reply] [copy] actions after the response
2572
- if (this.chatView && cleaned) {
2573
- const t = theme();
2574
- const teammate = event.result.teammate;
2575
- const replyId = `reply-${teammate}-${Date.now()}`;
2576
- this._replyContexts.set(replyId, { teammate, message: cleaned });
2577
- this.chatView.appendActionList([
2578
- {
2579
- id: replyId,
2580
- normalStyle: this.makeSpan({
2581
- text: " [reply]",
2582
- style: { fg: t.textDim },
2583
- }),
2584
- hoverStyle: this.makeSpan({
2585
- text: " [reply]",
2586
- style: { fg: t.accent },
2587
- }),
2588
- },
2589
- {
2590
- id: "copy",
2591
- normalStyle: this.makeSpan({
2592
- text: " [copy]",
2593
- style: { fg: t.textDim },
2594
- }),
2595
- hoverStyle: this.makeSpan({
2596
- text: " [copy]",
2597
- style: { fg: t.accent },
2598
- }),
2599
- },
2600
- ]);
2601
- }
2602
- this.feedLine();
2603
- // Auto-detect new teammates added during this task
2604
- this.refreshTeammates();
2605
- this.showPrompt();
2606
3091
  break;
2607
3092
  }
2608
3093
  case "error":
@@ -2611,7 +3096,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2611
3096
  this.stopStatusAnimation();
2612
3097
  if (!this.chatView)
2613
3098
  this.input.deactivateAndErase();
2614
- this.feedLine(tp.error(` ✖ ${event.teammate}: ${event.error}`));
3099
+ const displayErr = event.teammate === this.selfName ? this.adapterName : event.teammate;
3100
+ this.feedLine(tp.error(` ✖ ${displayErr}: ${event.error}`));
2615
3101
  this.showPrompt();
2616
3102
  break;
2617
3103
  }
@@ -2622,16 +3108,34 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2622
3108
  this.feedLine();
2623
3109
  this.feedLine(tp.bold(" Status"));
2624
3110
  this.feedLine(tp.muted(` ${"─".repeat(50)}`));
3111
+ // Show user avatar first if present (displayed as adapter name alias)
3112
+ if (this.userAlias) {
3113
+ const userStatus = statuses.get(this.userAlias);
3114
+ if (userStatus) {
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."));
3117
+ this.feedLine();
3118
+ }
3119
+ }
2625
3120
  for (const [name, status] of statuses) {
3121
+ // Skip the user avatar (shown above) and adapter fallback (not addressable)
3122
+ if (name === this.adapterName || name === this.userAlias)
3123
+ continue;
2626
3124
  const t = registry.get(name);
2627
3125
  const active = this.agentActive.get(name);
2628
3126
  const queued = this.taskQueue.filter((e) => e.teammate === name);
3127
+ // Presence indicator: ● green=online, ● red=offline, ● yellow=reachable
3128
+ const presenceIcon = status.presence === "online"
3129
+ ? tp.success("●")
3130
+ : status.presence === "reachable"
3131
+ ? tp.warning("●")
3132
+ : tp.error("●");
2629
3133
  // Teammate name + state
2630
3134
  const stateLabel = active ? "working" : status.state;
2631
3135
  const stateColor = stateLabel === "working"
2632
3136
  ? tp.info(` (${stateLabel})`)
2633
3137
  : tp.muted(` (${stateLabel})`);
2634
- this.feedLine(concat(tp.accent(` @${name}`), stateColor));
3138
+ this.feedLine(concat(presenceIcon, tp.accent(` @${name}`), stateColor));
2635
3139
  // Role
2636
3140
  if (t) {
2637
3141
  this.feedLine(tp.muted(` ${t.role}`));
@@ -2669,7 +3173,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2669
3173
  // Pick all teammates with debug files, queue one analysis per teammate
2670
3174
  const names = [];
2671
3175
  for (const [name] of this.lastDebugFiles) {
2672
- if (name !== this.adapterName)
3176
+ if (name !== this.selfName)
2673
3177
  names.push(name);
2674
3178
  }
2675
3179
  if (names.length === 0) {
@@ -2734,7 +3238,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2734
3238
  this.refreshView();
2735
3239
  this.taskQueue.push({
2736
3240
  type: "debug",
2737
- teammate: this.adapterName,
3241
+ teammate: this.selfName,
2738
3242
  task: analysisPrompt,
2739
3243
  });
2740
3244
  this.kickDrain();
@@ -2752,7 +3256,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2752
3256
  return;
2753
3257
  }
2754
3258
  const removed = this.taskQueue.splice(n - 1, 1)[0];
2755
- this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${removed.teammate}`), tp.muted(" "), tp.text(removed.task.slice(0, 60))));
3259
+ const cancelDisplay = removed.teammate === this.selfName ? this.adapterName : removed.teammate;
3260
+ this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
2756
3261
  this.refreshView();
2757
3262
  }
2758
3263
  /** Drain tasks for a single agent — runs in parallel with other agents. */
@@ -2787,11 +3292,54 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2787
3292
  const extraContext = entry.type === "btw" || entry.type === "debug"
2788
3293
  ? ""
2789
3294
  : this.buildConversationContext();
2790
- const result = await this.orchestrator.assign({
3295
+ let result = await this.orchestrator.assign({
2791
3296
  teammate: entry.teammate,
2792
3297
  task: entry.task,
2793
3298
  extraContext: extraContext || undefined,
2794
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);
2795
3343
  // Write debug entry — skip for debug analysis tasks (avoid recursion)
2796
3344
  if (entry.type !== "debug") {
2797
3345
  this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
@@ -2815,7 +3363,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2815
3363
  if (this.activeTasks.size === 0)
2816
3364
  this.stopStatusAnimation();
2817
3365
  const msg = err?.message ?? String(err);
2818
- this.feedLine(tp.error(` ✖ @${agent}: ${msg}`));
3366
+ const displayAgent = agent === this.selfName ? this.adapterName : agent;
3367
+ this.feedLine(tp.error(` ✖ @${displayAgent}: ${msg}`));
2819
3368
  this.refreshView();
2820
3369
  }
2821
3370
  this.agentActive.delete(agent);
@@ -2916,7 +3465,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2916
3465
  const teammatesDir = join(cwd, ".teammates");
2917
3466
  await mkdir(teammatesDir, { recursive: true });
2918
3467
  const fromPath = argsStr.trim();
2919
- if (fromPath) {
3468
+ if (fromPath === "pick") {
3469
+ // Persona picker mode: /init pick
3470
+ await this.runPersonaOnboardingInline(teammatesDir);
3471
+ }
3472
+ else if (fromPath) {
2920
3473
  // Import mode: /init <path-to-another-project>
2921
3474
  const resolved = resolve(fromPath);
2922
3475
  let sourceDir;
@@ -2950,11 +3503,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2950
3503
  // Copy framework files so the agent has TEMPLATE.md etc. available
2951
3504
  await copyTemplateFiles(teammatesDir);
2952
3505
  // Queue a single adaptation task that handles all teammates
2953
- this.feedLine(tp.muted(` Queuing ${this.adapterName} to scan this project and adapt the team...`));
3506
+ this.feedLine(tp.muted(" Queuing agent to scan this project and adapt the team..."));
2954
3507
  const prompt = await buildImportAdaptationPrompt(teammatesDir, allTeammates, sourceDir);
2955
3508
  this.taskQueue.push({
2956
3509
  type: "agent",
2957
- teammate: this.adapterName,
3510
+ teammate: this.selfName,
2958
3511
  task: prompt,
2959
3512
  });
2960
3513
  this.kickDrain();
@@ -3014,9 +3567,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3014
3567
  return;
3015
3568
  const registry = this.orchestrator.getRegistry();
3016
3569
  // Update adapter roster so prompts include the new teammates
3570
+ // Exclude the user avatar and adapter fallback — neither is an addressable teammate
3017
3571
  if ("roster" in this.adapter) {
3018
3572
  this.adapter.roster = this.orchestrator
3019
3573
  .listTeammates()
3574
+ .filter((n) => n !== this.adapterName && n !== this.userAlias)
3020
3575
  .map((name) => {
3021
3576
  const t = registry.get(name);
3022
3577
  return { name: t.name, role: t.role, ownership: t.ownership };
@@ -3038,7 +3593,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3038
3593
  const arg = argsStr.trim().replace(/^@/, "");
3039
3594
  const allTeammates = this.orchestrator
3040
3595
  .listTeammates()
3041
- .filter((n) => n !== this.adapterName);
3596
+ .filter((n) => n !== this.selfName && n !== this.adapterName);
3042
3597
  const names = !arg || arg === "everyone" ? allTeammates : [arg];
3043
3598
  // Validate all names first
3044
3599
  const valid = [];
@@ -3109,7 +3664,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3109
3664
  if (spinner)
3110
3665
  spinner.succeed(`${name}: ${parts.join(", ")}`);
3111
3666
  if (this.chatView)
3112
- this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
3667
+ this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
3113
3668
  }
3114
3669
  if (this.chatView)
3115
3670
  this.chatView.setProgress(null);
@@ -3131,7 +3686,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3131
3686
  syncSpinner.succeed(`${name}: index synced`);
3132
3687
  if (this.chatView) {
3133
3688
  this.chatView.setProgress(null);
3134
- this.feedLine(tp.success(` ✔ ${name}: index synced`));
3689
+ this.feedLine(tp.success(` ✔ ${name}: index synced`));
3135
3690
  }
3136
3691
  }
3137
3692
  catch {
@@ -3161,7 +3716,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3161
3716
  spinner.fail(`${name}: ${msg}`);
3162
3717
  if (this.chatView) {
3163
3718
  this.chatView.setProgress(null);
3164
- this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3719
+ this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3165
3720
  }
3166
3721
  }
3167
3722
  this.refreshView();
@@ -3171,7 +3726,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3171
3726
  // Resolve target list
3172
3727
  const allTeammates = this.orchestrator
3173
3728
  .listTeammates()
3174
- .filter((n) => n !== this.adapterName);
3729
+ .filter((n) => n !== this.selfName && n !== this.adapterName);
3175
3730
  let targets;
3176
3731
  if (arg === "everyone") {
3177
3732
  targets = allTeammates;
@@ -3284,7 +3839,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3284
3839
  }
3285
3840
  const teammates = this.orchestrator
3286
3841
  .listTeammates()
3287
- .filter((n) => n !== this.adapterName);
3842
+ .filter((n) => n !== this.selfName && n !== this.adapterName);
3288
3843
  if (teammates.length === 0)
3289
3844
  return;
3290
3845
  // 1. Check each teammate for stale daily logs (older than 7 days)
@@ -3369,7 +3924,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3369
3924
  child.stdin?.end();
3370
3925
  // Show brief "Copied" message in the progress area
3371
3926
  if (this.chatView) {
3372
- this.chatView.setProgress(concat(tp.success("✔ "), tp.muted("Copied to clipboard")));
3927
+ this.chatView.setProgress(concat(tp.success("✔ "), tp.muted("Copied to clipboard")));
3373
3928
  this.refreshView();
3374
3929
  setTimeout(() => {
3375
3930
  this.chatView.setProgress(null);
@@ -3379,7 +3934,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3379
3934
  }
3380
3935
  catch {
3381
3936
  if (this.chatView) {
3382
- this.chatView.setProgress(concat(tp.error("✖ "), tp.muted("Failed to copy")));
3937
+ this.chatView.setProgress(concat(tp.error("✖ "), tp.muted("Failed to copy")));
3383
3938
  this.refreshView();
3384
3939
  setTimeout(() => {
3385
3940
  this.chatView.setProgress(null);
@@ -3388,6 +3943,19 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3388
3943
  }
3389
3944
  }
3390
3945
  }
3946
+ /**
3947
+ * Feed a command line with a clickable [copy] button.
3948
+ * Renders as: ` command text [copy]`
3949
+ */
3950
+ feedCommand(command) {
3951
+ if (!this.chatView) {
3952
+ this.feedLine(tp.accent(` ${command}`));
3953
+ return;
3954
+ }
3955
+ const normal = concat(tp.accent(` ${command} `), tp.muted("[copy]"));
3956
+ const hover = concat(tp.accent(` ${command} `), tp.accent("[copy]"));
3957
+ this.chatView.appendAction(`copy-cmd:${command}`, normal, hover);
3958
+ }
3391
3959
  async cmdHelp() {
3392
3960
  this.feedLine();
3393
3961
  this.feedLine(tp.bold(" Commands"));
@@ -3439,9 +4007,9 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3439
4007
  this.refreshView();
3440
4008
  return;
3441
4009
  }
3442
- // Has args — queue a task to the coding agent to apply the change
4010
+ // Has args — queue a task to apply the change
3443
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.`;
3444
- this.taskQueue.push({ type: "agent", teammate: this.adapterName, task });
4012
+ this.taskQueue.push({ type: "agent", teammate: this.selfName, task });
3445
4013
  this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.adapterName}`)));
3446
4014
  this.feedLine();
3447
4015
  this.refreshView();
@@ -3456,7 +4024,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3456
4024
  }
3457
4025
  this.taskQueue.push({
3458
4026
  type: "btw",
3459
- teammate: this.adapterName,
4027
+ teammate: this.selfName,
3460
4028
  task: question,
3461
4029
  });
3462
4030
  this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.adapterName}`)));
@@ -3488,9 +4056,9 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3488
4056
  row("textDim", t.textDim, "─── separator ───");
3489
4057
  this.feedLine();
3490
4058
  // Status
3491
- row("success", t.success, "✔ Task completed");
3492
- row("warning", t.warning, "⚠ Pending handoff");
3493
- row("error", t.error, "✖ Something went wrong");
4059
+ row("success", t.success, "✔ Task completed");
4060
+ row("warning", t.warning, "⚠ Pending handoff");
4061
+ row("error", t.error, "✖ Something went wrong");
3494
4062
  row("info", t.info, "⠋ Working on task...");
3495
4063
  this.feedLine();
3496
4064
  // Interactive
@@ -3558,9 +4126,9 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3558
4126
  "",
3559
4127
  "| Language | Status |",
3560
4128
  "|------------|---------|",
3561
- "| JavaScript | ✔ Ready |",
3562
- "| Python | ✔ Ready |",
3563
- "| C# | ✔ Ready |",
4129
+ "| JavaScript | ✔ Ready |",
4130
+ "| Python | ✔ Ready |",
4131
+ "| C# | ✔ Ready |",
3564
4132
  "",
3565
4133
  "---",
3566
4134
  ].join("\n");