@teammates/cli 0.3.4 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,13 +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
- import { tmpdir } from "node:os";
14
13
  import { dirname, join, resolve } from "node:path";
15
14
  import { createInterface } from "node:readline";
16
- import { App, ChatView, concat, esc, Interview, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
15
+ import { App, ChatView, concat, esc, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
17
16
  import chalk from "chalk";
18
17
  import ora from "ora";
19
18
  import { syncRecallIndex } from "./adapter.js";
@@ -39,24 +38,78 @@ class TeammatesREPL {
39
38
  lastResult = null;
40
39
  lastResults = new Map();
41
40
  conversationHistory = [];
41
+ /** Running summary of older conversation history maintained by the coding agent. */
42
+ conversationSummary = "";
42
43
  storeResult(result) {
43
44
  this.lastResult = result;
44
45
  this.lastResults.set(result.teammate, result);
45
46
  this.conversationHistory.push({
46
47
  role: result.teammate,
47
- text: result.rawOutput ?? result.summary,
48
+ text: result.summary,
48
49
  });
49
50
  }
51
+ /** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
52
+ static CONV_HISTORY_CHARS = 24_000 * 4;
50
53
  buildConversationContext() {
51
- if (this.conversationHistory.length === 0)
54
+ if (this.conversationHistory.length === 0 && !this.conversationSummary)
52
55
  return "";
53
- // Keep last 10 exchanges to avoid blowing up prompt size
54
- const recent = this.conversationHistory.slice(-10);
55
- const lines = ["## Conversation History\n"];
56
- for (const entry of recent) {
57
- lines.push(`**${entry.role}:** ${entry.text}\n`);
56
+ const budget = TeammatesREPL.CONV_HISTORY_CHARS;
57
+ const parts = ["## Conversation History\n"];
58
+ // Include running summary of older conversation if present
59
+ if (this.conversationSummary) {
60
+ parts.push(`### Previous Conversation Summary\n\n${this.conversationSummary}\n`);
61
+ }
62
+ // Work backwards from newest — include whole entries up to 24k tokens
63
+ const entries = [];
64
+ let used = 0;
65
+ for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
66
+ const line = `**${this.conversationHistory[i].role}:** ${this.conversationHistory[i].text}\n`;
67
+ if (used + line.length > budget && entries.length > 0)
68
+ break;
69
+ entries.unshift(line);
70
+ used += line.length;
58
71
  }
59
- return lines.join("\n");
72
+ if (entries.length > 0)
73
+ parts.push(entries.join("\n"));
74
+ return parts.join("\n");
75
+ }
76
+ /**
77
+ * Check if conversation history exceeds the 24k token budget.
78
+ * If so, take the older entries that won't fit, combine with existing summary,
79
+ * and queue a summarization task to the coding agent.
80
+ */
81
+ maybeQueueSummarization() {
82
+ const budget = TeammatesREPL.CONV_HISTORY_CHARS;
83
+ // Calculate how many recent entries fit in the budget (newest first)
84
+ let recentChars = 0;
85
+ let splitIdx = this.conversationHistory.length;
86
+ for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
87
+ const line = `**${this.conversationHistory[i].role}:** ${this.conversationHistory[i].text}\n`;
88
+ if (recentChars + line.length > budget)
89
+ break;
90
+ recentChars += line.length;
91
+ splitIdx = i;
92
+ }
93
+ if (splitIdx === 0)
94
+ return; // everything fits — nothing to summarize
95
+ // Collect entries that are being pushed out
96
+ const toSummarize = this.conversationHistory.slice(0, splitIdx);
97
+ const entriesText = toSummarize
98
+ .map((e) => `**${e.role}:** ${e.text}`)
99
+ .join("\n");
100
+ // Build the summarization prompt
101
+ const prompt = this.conversationSummary
102
+ ? `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Update the existing summary to incorporate the new conversation entries below.\n\n## Current Summary\n\n${this.conversationSummary}\n\n## New Entries to Incorporate\n\n${entriesText}\n\n## Instructions\n\nReturn ONLY the updated 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)`
103
+ : `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
+ // Remove the summarized entries — they'll be captured in the summary
105
+ this.conversationHistory.splice(0, splitIdx);
106
+ // Queue the summarization task through the user's agent
107
+ this.taskQueue.push({
108
+ type: "summarize",
109
+ teammate: this.selfName,
110
+ task: prompt,
111
+ });
112
+ this.kickDrain();
60
113
  }
61
114
  adapterName;
62
115
  teammatesDir;
@@ -89,9 +142,16 @@ class TeammatesREPL {
89
142
  _replyContexts = new Map();
90
143
  /** Quoted reply text to expand on next submit. */
91
144
  _pendingQuotedReply = null;
92
- defaultFooter = null; // cached default footer content
145
+ /** Resolver for inline ask when set, next submit resolves this instead of normal handling. */
146
+ _pendingAsk = null;
147
+ defaultFooter = null; // cached left footer content
148
+ defaultFooterRight = null; // cached right footer content
93
149
  /** Cached service statuses for banner + /configure. */
94
150
  serviceStatuses = [];
151
+ /** Reference to the animated banner widget for live updates. */
152
+ banner = null;
153
+ /** The local user's alias (avatar name). Set after USER.md is read or interview completes. */
154
+ userAlias = null;
95
155
  // ── Animated status tracker ─────────────────────────────────────
96
156
  activeTasks = new Map();
97
157
  statusTimer = null;
@@ -113,6 +173,13 @@ class TeammatesREPL {
113
173
  constructor(adapterName) {
114
174
  this.adapterName = adapterName;
115
175
  }
176
+ /**
177
+ * The name used for the local user in the roster.
178
+ * Returns the user's alias if set, otherwise the adapter name.
179
+ */
180
+ get selfName() {
181
+ return this.userAlias ?? this.adapterName;
182
+ }
116
183
  /** Show the prompt with the fenced border. */
117
184
  showPrompt() {
118
185
  if (this.chatView) {
@@ -168,27 +235,28 @@ class TeammatesREPL {
168
235
  const entries = Array.from(this.activeTasks.values());
169
236
  const idx = this.statusRotateIndex % entries.length;
170
237
  const { teammate, task } = entries[idx];
238
+ const displayName = teammate === this.adapterName ? this.selfName : teammate;
171
239
  const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
172
240
  const taskPreview = task.length > 50 ? `${task.slice(0, 47)}...` : task;
173
241
  const queueInfo = this.activeTasks.size > 1 ? ` (${idx + 1}/${this.activeTasks.size})` : "";
174
242
  if (this.chatView) {
175
243
  // Strip newlines and truncate task text for single-line display
176
244
  const cleanTask = task.replace(/[\r\n]+/g, " ").trim();
177
- const maxLen = Math.max(20, (process.stdout.columns || 80) - teammate.length - 10);
245
+ const maxLen = Math.max(20, (process.stdout.columns || 80) - displayName.length - 10);
178
246
  const taskText = cleanTask.length > maxLen
179
247
  ? `${cleanTask.slice(0, maxLen - 1)}…`
180
248
  : cleanTask;
181
249
  const queueTag = this.activeTasks.size > 1
182
250
  ? ` (${idx + 1}/${this.activeTasks.size})`
183
251
  : "";
184
- this.chatView.setProgress(concat(tp.accent(`${spinChar} ${teammate}… `), tp.muted(taskText + queueTag)));
252
+ this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}… `), tp.muted(taskText + queueTag)));
185
253
  this.app.refresh();
186
254
  }
187
255
  else {
188
256
  // Mostly bright blue, periodically flicker to dark blue
189
257
  const spinColor = this.statusFrame % 8 === 0 ? chalk.blue : chalk.blueBright;
190
258
  const line = ` ${spinColor(spinChar)} ` +
191
- chalk.bold(teammate) +
259
+ chalk.bold(displayName) +
192
260
  chalk.gray(`… ${taskPreview}`) +
193
261
  (queueInfo ? chalk.gray(queueInfo) : "");
194
262
  this.input.setStatus(line);
@@ -246,8 +314,8 @@ class TeammatesREPL {
246
314
  rendered.push({ type: "text", content: line });
247
315
  }
248
316
  }
249
- // Render first line with "User: " label
250
- const label = "user: ";
317
+ // Render first line with alias label
318
+ const label = `${this.selfName}: `;
251
319
  const first = rendered.shift();
252
320
  if (first) {
253
321
  if (first.type === "text") {
@@ -430,7 +498,7 @@ class TeammatesREPL {
430
498
  style: { fg: chrome },
431
499
  }));
432
500
  if (!isValid) {
433
- this.feedLine(tp.error(` ✖ Unknown teammate: @${h.to}`));
501
+ this.feedLine(tp.error(` ✖ Unknown teammate: @${h.to}`));
434
502
  }
435
503
  else if (this.autoApproveHandoffs) {
436
504
  this.taskQueue.push({ type: "agent", teammate: h.to, task: h.task });
@@ -794,13 +862,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
794
862
  if (this.app)
795
863
  this.app.refresh();
796
864
  }
797
- queueTask(input) {
865
+ queueTask(input, preMentions) {
798
866
  const allNames = this.orchestrator.listTeammates();
799
867
  // Check for @everyone — queue to all teammates except the coding agent
800
868
  const everyoneMatch = input.match(/^@everyone\s+([\s\S]+)$/i);
801
869
  if (everyoneMatch) {
802
870
  const task = everyoneMatch[1];
803
- const names = allNames.filter((n) => n !== this.adapterName);
871
+ const names = allNames.filter((n) => n !== this.selfName && n !== this.adapterName);
804
872
  for (const teammate of names) {
805
873
  this.taskQueue.push({ type: "agent", teammate, task });
806
874
  }
@@ -812,14 +880,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
812
880
  this.kickDrain();
813
881
  return;
814
882
  }
815
- // Collect all @mentioned teammates anywhere in the input
816
- const mentionRegex = /@(\S+)/g;
817
- let m;
818
- const mentioned = [];
819
- while ((m = mentionRegex.exec(input)) !== null) {
820
- const name = m[1];
821
- if (allNames.includes(name) && !mentioned.includes(name)) {
822
- mentioned.push(name);
883
+ // Use pre-resolved mentions if provided (avoids picking up @mentions from expanded paste text),
884
+ // otherwise scan the input directly.
885
+ let mentioned;
886
+ if (preMentions) {
887
+ mentioned = preMentions;
888
+ }
889
+ else {
890
+ const mentionRegex = /@(\S+)/g;
891
+ let m;
892
+ mentioned = [];
893
+ while ((m = mentionRegex.exec(input)) !== null) {
894
+ const name = m[1];
895
+ if (allNames.includes(name) && !mentioned.includes(name)) {
896
+ mentioned.push(name);
897
+ }
823
898
  }
824
899
  }
825
900
  if (mentioned.length > 0) {
@@ -841,12 +916,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
841
916
  match = this.lastResult.teammate;
842
917
  }
843
918
  if (!match) {
844
- match = this.orchestrator.route(input) ?? this.adapterName;
919
+ match = this.orchestrator.route(input) ?? this.selfName;
845
920
  }
846
921
  {
847
922
  const bg = this._userBg;
848
923
  const t = theme();
849
- this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${match}`)));
924
+ const displayName = match === this.adapterName ? this.selfName : match;
925
+ this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${displayName}`)));
850
926
  }
851
927
  this.feedLine();
852
928
  this.refreshView();
@@ -871,20 +947,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
871
947
  }
872
948
  // ─── Onboarding ───────────────────────────────────────────────────
873
949
  /**
874
- * Interactive prompt when no .teammates/ directory is found.
875
- * Returns the new .teammates/ path, or null if user chose to exit.
950
+ * Interactive prompt for team onboarding after user profile is set up.
951
+ * .teammates/ already exists at this point. Returns false if user chose to exit.
876
952
  */
877
- async promptOnboarding(adapter) {
953
+ async promptTeamOnboarding(adapter, teammatesDir) {
878
954
  const cwd = process.cwd();
879
- const teammatesDir = join(cwd, ".teammates");
880
955
  const termWidth = process.stdout.columns || 100;
881
956
  console.log();
882
- this.printLogo([
883
- chalk.bold("Teammates") + chalk.gray(` v${PKG_VERSION}`),
884
- chalk.yellow("No .teammates/ directory found"),
885
- chalk.gray(cwd),
886
- ]);
887
- console.log();
888
957
  console.log(chalk.gray("─".repeat(termWidth)));
889
958
  console.log();
890
959
  console.log(chalk.white(" Set up teammates for this project?\n"));
@@ -899,7 +968,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
899
968
  console.log(chalk.cyan(" 3") +
900
969
  chalk.gray(") ") +
901
970
  chalk.white("Solo mode") +
902
- chalk.gray(` — use ${this.adapterName} without teammates`));
971
+ chalk.gray(" — use your agent without teammates"));
903
972
  console.log(chalk.cyan(" 4") + chalk.gray(") ") + chalk.white("Exit"));
904
973
  console.log();
905
974
  const choice = await this.askChoice("Pick an option (1/2/3/4): ", [
@@ -910,27 +979,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
910
979
  ]);
911
980
  if (choice === "4") {
912
981
  console.log(chalk.gray(" Goodbye."));
913
- return null;
982
+ return false;
914
983
  }
915
984
  if (choice === "3") {
916
- await mkdir(teammatesDir, { recursive: true });
917
- console.log();
918
- console.log(chalk.green(" ✔") + chalk.gray(` Created ${teammatesDir}`));
919
- console.log(chalk.gray(` Running in solo mode — all tasks go to ${this.adapterName}.`));
985
+ console.log(chalk.gray(" Running in solo mode — all tasks go to your agent."));
920
986
  console.log(chalk.gray(" Run /init later to set up teammates."));
921
987
  console.log();
922
- return teammatesDir;
988
+ return true;
923
989
  }
924
990
  if (choice === "2") {
925
- // Import from another project
926
- await mkdir(teammatesDir, { recursive: true });
927
991
  await this.runImport(cwd);
928
- return teammatesDir;
992
+ return true;
929
993
  }
930
994
  // choice === "1": Run onboarding via the agent
931
- await mkdir(teammatesDir, { recursive: true });
932
995
  await this.runOnboardingAgent(adapter, cwd);
933
- return teammatesDir;
996
+ return true;
934
997
  }
935
998
  /**
936
999
  * Run the onboarding agent to analyze the codebase and create teammates.
@@ -939,19 +1002,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
939
1002
  async runOnboardingAgent(adapter, projectDir) {
940
1003
  console.log();
941
1004
  console.log(chalk.blue(" Starting onboarding...") +
942
- chalk.gray(` ${this.adapterName} will analyze your codebase and create .teammates/`));
1005
+ chalk.gray(" Your agent will analyze your codebase and create .teammates/"));
943
1006
  console.log();
944
1007
  // Copy framework files from bundled template
945
1008
  const teammatesDir = join(projectDir, ".teammates");
946
1009
  const copied = await copyTemplateFiles(teammatesDir);
947
1010
  if (copied.length > 0) {
948
- console.log(chalk.green(" ✔") +
1011
+ console.log(chalk.green(" ✔ ") +
949
1012
  chalk.gray(` Copied template files: ${copied.join(", ")}`));
950
1013
  console.log();
951
1014
  }
952
1015
  const onboardingPrompt = await getOnboardingPrompt(projectDir);
953
1016
  const tempConfig = {
954
1017
  name: this.adapterName,
1018
+ type: "ai",
955
1019
  role: "Onboarding agent",
956
1020
  soul: "",
957
1021
  wisdom: "",
@@ -963,8 +1027,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
963
1027
  };
964
1028
  const sessionId = await adapter.startSession(tempConfig);
965
1029
  const spinner = ora({
966
- text: chalk.blue(this.adapterName) +
967
- chalk.gray(" is analyzing your codebase..."),
1030
+ text: chalk.gray("Analyzing your codebase..."),
968
1031
  spinner: "dots",
969
1032
  }).start();
970
1033
  try {
@@ -972,7 +1035,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
972
1035
  spinner.stop();
973
1036
  this.printAgentOutput(result.rawOutput);
974
1037
  if (result.success) {
975
- console.log(chalk.green(" ✔ Onboarding complete!"));
1038
+ console.log(chalk.green(" ✔ Onboarding complete!"));
976
1039
  }
977
1040
  else {
978
1041
  console.log(chalk.yellow(` ⚠ Onboarding finished with issues: ${result.summary}`));
@@ -1038,7 +1101,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1038
1101
  return;
1039
1102
  }
1040
1103
  if (teammates.length > 0) {
1041
- console.log(chalk.green(" ✔") +
1104
+ console.log(chalk.green(" ✔ ") +
1042
1105
  chalk.white(` Imported ${teammates.length} teammate${teammates.length > 1 ? "s" : ""}: `) +
1043
1106
  chalk.cyan(teammates.join(", ")));
1044
1107
  console.log(chalk.gray(` (${files.length} files copied)`));
@@ -1078,11 +1141,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1078
1141
  const teammatesDir = join(projectDir, ".teammates");
1079
1142
  console.log();
1080
1143
  console.log(chalk.blue(" Starting adaptation...") +
1081
- chalk.gray(` ${this.adapterName} will scan this project and adapt the team`));
1144
+ chalk.gray(" Your agent will scan this project and adapt the team"));
1082
1145
  console.log();
1083
1146
  const prompt = await buildImportAdaptationPrompt(teammatesDir, teammateNames, sourceProjectPath);
1084
1147
  const tempConfig = {
1085
1148
  name: this.adapterName,
1149
+ type: "ai",
1086
1150
  role: "Adaptation agent",
1087
1151
  soul: "",
1088
1152
  wisdom: "",
@@ -1094,8 +1158,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1094
1158
  };
1095
1159
  const sessionId = await adapter.startSession(tempConfig);
1096
1160
  const spinner = ora({
1097
- text: chalk.blue(this.adapterName) +
1098
- chalk.gray(" is scanning the project and adapting teammates..."),
1161
+ text: chalk.gray("Scanning the project and adapting teammates..."),
1099
1162
  spinner: "dots",
1100
1163
  }).start();
1101
1164
  try {
@@ -1103,7 +1166,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1103
1166
  spinner.stop();
1104
1167
  this.printAgentOutput(result.rawOutput);
1105
1168
  if (result.success) {
1106
- console.log(chalk.green(" ✔ Team adaptation complete!"));
1169
+ console.log(chalk.green(" ✔ Team adaptation complete!"));
1107
1170
  }
1108
1171
  else {
1109
1172
  console.log(chalk.yellow(` ⚠ Adaptation finished with issues: ${result.summary}`));
@@ -1153,6 +1216,30 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1153
1216
  });
1154
1217
  });
1155
1218
  }
1219
+ /**
1220
+ * Ask for input using the ChatView's own prompt (no raw readline).
1221
+ * Temporarily replaces the footer with the prompt text and intercepts the next submit.
1222
+ */
1223
+ askInline(prompt) {
1224
+ return new Promise((resolve) => {
1225
+ if (!this.chatView) {
1226
+ // Fallback if no ChatView (shouldn't happen during /configure)
1227
+ return this.askInput(prompt).then(resolve);
1228
+ }
1229
+ // Show the prompt in the feed so it's visible
1230
+ this.feedLine(tp.accent(` ${prompt}`));
1231
+ this.chatView.setFooter(tp.accent(` ${prompt}`));
1232
+ this._pendingAsk = (answer) => {
1233
+ // Restore footer
1234
+ if (this.chatView && this.defaultFooter) {
1235
+ this.chatView.setFooter(this.defaultFooter);
1236
+ }
1237
+ this.refreshView();
1238
+ resolve(answer.trim());
1239
+ };
1240
+ this.refreshView();
1241
+ });
1242
+ }
1156
1243
  /**
1157
1244
  * Check whether USER.md needs to be created or is still template placeholders.
1158
1245
  */
@@ -1169,71 +1256,309 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1169
1256
  }
1170
1257
  }
1171
1258
  /**
1172
- * Run the user interview inside the ChatView using the Interview widget.
1173
- * Hides the normal input prompt until the interview completes.
1259
+ * Pre-TUI user profile setup. Runs in the console before the ChatView is created.
1260
+ * Offers GitHub-based or manual profile creation.
1174
1261
  */
1175
- startUserInterview(teammatesDir, bannerWidget) {
1176
- if (!this.chatView)
1262
+ async runUserSetup(teammatesDir) {
1263
+ const termWidth = process.stdout.columns || 100;
1264
+ console.log();
1265
+ console.log(chalk.gray("─".repeat(termWidth)));
1266
+ console.log();
1267
+ console.log(chalk.white(" Set up your profile\n"));
1268
+ console.log(chalk.cyan(" 1") +
1269
+ chalk.gray(") ") +
1270
+ chalk.white("Use GitHub account") +
1271
+ chalk.gray(" — import your name and username from GitHub"));
1272
+ console.log(chalk.cyan(" 2") +
1273
+ chalk.gray(") ") +
1274
+ chalk.white("Manual setup") +
1275
+ chalk.gray(" — enter your details manually"));
1276
+ console.log(chalk.cyan(" 3") +
1277
+ chalk.gray(") ") +
1278
+ chalk.white("Skip") +
1279
+ chalk.gray(" — set up later with /user"));
1280
+ console.log();
1281
+ const choice = await this.askChoice("Pick an option (1/2/3): ", [
1282
+ "1",
1283
+ "2",
1284
+ "3",
1285
+ ]);
1286
+ if (choice === "3") {
1287
+ console.log(chalk.gray(" Skipped — run /user to set up your profile later."));
1288
+ console.log();
1177
1289
  return;
1178
- const t = theme();
1179
- const interview = new Interview({
1180
- title: "Quick intro — helps teammates tailor their work to you.",
1181
- subtitle: "(press Enter to skip any question)",
1182
- questions: [
1183
- { key: "name", prompt: "Your name" },
1184
- {
1185
- key: "role",
1186
- prompt: "Your role",
1187
- placeholder: "e.g., senior backend engineer",
1188
- },
1189
- {
1190
- key: "experience",
1191
- prompt: "Relevant experience",
1192
- placeholder: "e.g., 10 years Go, new to React",
1193
- },
1194
- {
1195
- key: "preferences",
1196
- prompt: "How you like to work",
1197
- placeholder: "e.g., terse responses, explain reasoning",
1198
- },
1199
- {
1200
- key: "context",
1201
- prompt: "Anything else",
1202
- placeholder: "e.g., solo dev, working on a rewrite",
1203
- },
1204
- ],
1205
- titleStyle: { fg: t.text },
1206
- subtitleStyle: { fg: t.textDim, italic: true },
1207
- promptStyle: { fg: t.accent },
1208
- answeredStyle: { fg: t.textMuted, italic: true },
1209
- inputStyle: { fg: t.text },
1210
- cursorStyle: { fg: t.cursorFg, bg: t.cursorBg },
1211
- placeholderStyle: { fg: t.textDim, italic: true },
1212
- });
1213
- this.chatView.setInputOverride(interview);
1214
- if (this.app)
1215
- this.app.refresh();
1216
- interview.on("complete", (answers) => {
1217
- // Write USER.md
1218
- const userMdPath = join(teammatesDir, "USER.md");
1219
- const lines = ["# User\n"];
1220
- lines.push(`- **Name:** ${answers.name || "_not provided_"}`);
1221
- lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
1222
- lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
1223
- lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
1224
- lines.push(`- **Context:** ${answers.context || "_not provided_"}`);
1225
- writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
1226
- // Remove override and restore normal input
1227
- if (this.chatView) {
1228
- this.chatView.setInputOverride(null);
1229
- this.chatView.appendStyledToFeed(concat(tp.success(" "), tp.dim("Saved USER.md — update anytime with /user")));
1230
- }
1231
- // Release the banner hold so commands animate in
1232
- if (bannerWidget)
1233
- bannerWidget.releaseHold();
1234
- if (this.app)
1235
- this.app.refresh();
1290
+ }
1291
+ if (choice === "1") {
1292
+ await this.setupGitHubProfile(teammatesDir);
1293
+ }
1294
+ else {
1295
+ await this.setupManualProfile(teammatesDir);
1296
+ }
1297
+ }
1298
+ /**
1299
+ * GitHub-based profile setup. Ensures gh is installed and authenticated,
1300
+ * then fetches user info from the GitHub API to create the profile.
1301
+ */
1302
+ async setupGitHubProfile(teammatesDir) {
1303
+ console.log();
1304
+ // Step 1: Check if gh is installed
1305
+ let ghInstalled = false;
1306
+ try {
1307
+ execSync("gh --version", { stdio: "pipe" });
1308
+ ghInstalled = true;
1309
+ }
1310
+ catch {
1311
+ // not installed
1312
+ }
1313
+ if (!ghInstalled) {
1314
+ console.log(chalk.yellow(" GitHub CLI is not installed.\n"));
1315
+ const plat = process.platform;
1316
+ console.log(chalk.white(" Run this in another terminal:"));
1317
+ if (plat === "win32") {
1318
+ console.log(chalk.cyan(" winget install --id GitHub.cli"));
1319
+ }
1320
+ else if (plat === "darwin") {
1321
+ console.log(chalk.cyan(" brew install gh"));
1322
+ }
1323
+ else {
1324
+ console.log(chalk.cyan(" sudo apt install gh"));
1325
+ console.log(chalk.gray(" (or see https://cli.github.com)"));
1326
+ }
1327
+ console.log();
1328
+ const answer = await this.askChoice("Press Enter when done, or s to skip: ", ["", "s", "S"]);
1329
+ if (answer.toLowerCase() === "s") {
1330
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1331
+ return this.setupManualProfile(teammatesDir);
1332
+ }
1333
+ // Re-check
1334
+ try {
1335
+ execSync("gh --version", { stdio: "pipe" });
1336
+ ghInstalled = true;
1337
+ console.log(chalk.green(" ✔ GitHub CLI installed"));
1338
+ }
1339
+ catch {
1340
+ console.log(chalk.yellow(" GitHub CLI still not found. You may need to restart your terminal."));
1341
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1342
+ return this.setupManualProfile(teammatesDir);
1343
+ }
1344
+ }
1345
+ else {
1346
+ console.log(chalk.green(" ✔ GitHub CLI installed"));
1347
+ }
1348
+ // Step 2: Check auth
1349
+ let authed = false;
1350
+ try {
1351
+ execSync("gh auth status", { stdio: "pipe" });
1352
+ authed = true;
1353
+ }
1354
+ catch {
1355
+ // not authenticated
1356
+ }
1357
+ if (!authed) {
1358
+ console.log();
1359
+ console.log(chalk.gray(" Authenticating with GitHub...\n"));
1360
+ const result = spawnSync("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
1361
+ stdio: "inherit",
1362
+ shell: true,
1363
+ });
1364
+ if (result.status !== 0) {
1365
+ console.log(chalk.yellow(" Authentication failed or was cancelled."));
1366
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1367
+ return this.setupManualProfile(teammatesDir);
1368
+ }
1369
+ // Verify
1370
+ try {
1371
+ execSync("gh auth status", { stdio: "pipe" });
1372
+ authed = true;
1373
+ }
1374
+ catch {
1375
+ console.log(chalk.yellow(" Authentication could not be verified."));
1376
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1377
+ return this.setupManualProfile(teammatesDir);
1378
+ }
1379
+ }
1380
+ console.log(chalk.green(" ✔ GitHub authenticated"));
1381
+ // Step 3: Fetch user info from GitHub API
1382
+ let login = "";
1383
+ let name = "";
1384
+ try {
1385
+ const json = execSync("gh api user", {
1386
+ stdio: "pipe",
1387
+ encoding: "utf-8",
1388
+ });
1389
+ const user = JSON.parse(json);
1390
+ login = (user.login || "").toLowerCase().replace(/[^a-z0-9_-]/g, "");
1391
+ name = user.name || user.login || "";
1392
+ }
1393
+ catch {
1394
+ console.log(chalk.yellow(" Could not fetch GitHub user info."));
1395
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1396
+ return this.setupManualProfile(teammatesDir);
1397
+ }
1398
+ if (!login) {
1399
+ console.log(chalk.yellow(" No GitHub username found."));
1400
+ console.log(chalk.gray(" Falling back to manual setup.\n"));
1401
+ return this.setupManualProfile(teammatesDir);
1402
+ }
1403
+ console.log(chalk.green(` ✔ Authenticated as `) +
1404
+ chalk.cyan(`@${login}`) +
1405
+ (name && name !== login ? chalk.gray(` (${name})`) : ""));
1406
+ console.log();
1407
+ // Ask for role (optional) since GitHub doesn't provide this
1408
+ const role = await this.askInput("Your role (optional, press Enter to skip): ");
1409
+ const answers = {
1410
+ alias: login,
1411
+ name: name || login,
1412
+ role: role || "",
1413
+ experience: "",
1414
+ preferences: "",
1415
+ context: "",
1416
+ };
1417
+ this.writeUserProfile(teammatesDir, login, answers);
1418
+ this.createUserAvatar(teammatesDir, login, answers);
1419
+ console.log(chalk.green(" ✔ ") +
1420
+ chalk.gray(`Profile created — avatar @${login}`));
1421
+ console.log();
1422
+ }
1423
+ /**
1424
+ * Manual (console-based) profile setup. Collects fields via askInput().
1425
+ */
1426
+ async setupManualProfile(teammatesDir) {
1427
+ console.log();
1428
+ console.log(chalk.gray(" (alias is required, press Enter to skip others)\n"));
1429
+ const aliasRaw = await this.askInput("Your alias (e.g., alex): ");
1430
+ const alias = aliasRaw.toLowerCase().replace(/[^a-z0-9_-]/g, "").trim();
1431
+ if (!alias) {
1432
+ console.log(chalk.yellow(" Alias is required. Run /user to try again.\n"));
1433
+ return;
1434
+ }
1435
+ const name = await this.askInput("Your name: ");
1436
+ const role = await this.askInput("Your role (e.g., senior backend engineer): ");
1437
+ const experience = await this.askInput("Relevant experience (e.g., 10 years Go, new to React): ");
1438
+ const preferences = await this.askInput("How you like to work (e.g., terse responses): ");
1439
+ const context = await this.askInput("Anything else: ");
1440
+ const answers = {
1441
+ alias,
1442
+ name,
1443
+ role,
1444
+ experience,
1445
+ preferences,
1446
+ context,
1447
+ };
1448
+ this.writeUserProfile(teammatesDir, alias, answers);
1449
+ this.createUserAvatar(teammatesDir, alias, answers);
1450
+ console.log();
1451
+ console.log(chalk.green(" ✔ ") +
1452
+ chalk.gray(`Profile created — avatar @${alias}`));
1453
+ console.log(chalk.gray(" Update anytime with /user"));
1454
+ console.log();
1455
+ }
1456
+ /**
1457
+ * Write USER.md from collected answers.
1458
+ */
1459
+ writeUserProfile(teammatesDir, alias, answers) {
1460
+ const userMdPath = join(teammatesDir, "USER.md");
1461
+ const lines = ["# User\n"];
1462
+ lines.push(`- **Alias:** ${alias}`);
1463
+ lines.push(`- **Name:** ${answers.name || "_not provided_"}`);
1464
+ lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
1465
+ lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
1466
+ lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
1467
+ lines.push(`- **Context:** ${answers.context || "_not provided_"}`);
1468
+ writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
1469
+ }
1470
+ /**
1471
+ * Create the user's avatar folder with SOUL.md and WISDOM.md.
1472
+ * The avatar is a teammate folder with type: human.
1473
+ */
1474
+ createUserAvatar(teammatesDir, alias, answers) {
1475
+ const avatarDir = join(teammatesDir, alias);
1476
+ const memoryDir = join(avatarDir, "memory");
1477
+ mkdirSync(avatarDir, { recursive: true });
1478
+ mkdirSync(memoryDir, { recursive: true });
1479
+ const name = answers.name || alias;
1480
+ const role = answers.role || "Team member";
1481
+ const experience = answers.experience || "";
1482
+ const preferences = answers.preferences || "";
1483
+ const context = answers.context || "";
1484
+ // Write SOUL.md
1485
+ const soulLines = [
1486
+ `# ${name}`,
1487
+ "",
1488
+ "## Identity",
1489
+ "",
1490
+ `**Type:** human`,
1491
+ `**Alias:** ${alias}`,
1492
+ `**Role:** ${role}`,
1493
+ ];
1494
+ if (experience)
1495
+ soulLines.push(`**Experience:** ${experience}`);
1496
+ if (preferences)
1497
+ soulLines.push(`**Preferences:** ${preferences}`);
1498
+ if (context) {
1499
+ soulLines.push("", "## Context", "", context);
1500
+ }
1501
+ soulLines.push("");
1502
+ const soulPath = join(avatarDir, "SOUL.md");
1503
+ writeFileSync(soulPath, soulLines.join("\n"), "utf-8");
1504
+ // Write empty WISDOM.md
1505
+ const wisdomPath = join(avatarDir, "WISDOM.md");
1506
+ writeFileSync(wisdomPath, `# ${name} — Wisdom\n\nDistilled from work history. Updated during compaction.\n`, "utf-8");
1507
+ // Avatar registration happens later in start() after the orchestrator is initialized.
1508
+ // During pre-TUI setup, the orchestrator doesn't exist yet.
1509
+ }
1510
+ /**
1511
+ * Read USER.md and extract the alias field.
1512
+ * Returns null if USER.md doesn't exist or has no alias.
1513
+ */
1514
+ readUserAlias(teammatesDir) {
1515
+ try {
1516
+ const content = readFileSync(join(teammatesDir, "USER.md"), "utf-8");
1517
+ const match = content.match(/\*\*Alias:\*\*\s*(\S+)/);
1518
+ return match ? match[1].toLowerCase().replace(/[^a-z0-9_-]/g, "") : null;
1519
+ }
1520
+ catch {
1521
+ return null;
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Register the user's avatar as a teammate in the orchestrator.
1526
+ * Sets presence to "online" since the local user is always online.
1527
+ * Replaces the old coding agent entry.
1528
+ */
1529
+ registerUserAvatar(teammatesDir, alias) {
1530
+ const registry = this.orchestrator.getRegistry();
1531
+ const avatarDir = join(teammatesDir, alias);
1532
+ // Read the avatar's SOUL.md if it exists
1533
+ let soul = "";
1534
+ let role = "Team member";
1535
+ try {
1536
+ soul = readFileSync(join(avatarDir, "SOUL.md"), "utf-8");
1537
+ const roleMatch = soul.match(/\*\*Role:\*\*\s*(.+)/);
1538
+ if (roleMatch)
1539
+ role = roleMatch[1].trim();
1540
+ }
1541
+ catch { /* avatar folder may not exist yet */ }
1542
+ let wisdom = "";
1543
+ try {
1544
+ wisdom = readFileSync(join(avatarDir, "WISDOM.md"), "utf-8");
1545
+ }
1546
+ catch { /* ok */ }
1547
+ registry.register({
1548
+ name: alias,
1549
+ type: "human",
1550
+ role,
1551
+ soul,
1552
+ wisdom,
1553
+ dailyLogs: [],
1554
+ weeklyLogs: [],
1555
+ ownership: { primary: [], secondary: [] },
1556
+ routingKeywords: [],
1236
1557
  });
1558
+ // Set presence to online (local user is always online)
1559
+ this.orchestrator.getAllStatuses().set(alias, { state: "idle", presence: "online" });
1560
+ // Update the adapter name so tasks route to the avatar
1561
+ this.userAlias = alias;
1237
1562
  }
1238
1563
  // ─── Display helpers ──────────────────────────────────────────────
1239
1564
  /**
@@ -1560,15 +1885,29 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1560
1885
  agentPassthrough: cliArgs.agentPassthrough,
1561
1886
  });
1562
1887
  this.adapter = adapter;
1563
- // No .teammates/ found offer onboarding or solo mode
1888
+ // Detect whether this is a brand-new project (no .teammates/ at all)
1889
+ const isNewProject = !teammatesDir;
1564
1890
  if (!teammatesDir) {
1565
- teammatesDir = await this.promptOnboarding(adapter);
1566
- if (!teammatesDir)
1891
+ teammatesDir = join(process.cwd(), ".teammates");
1892
+ await mkdir(teammatesDir, { recursive: true });
1893
+ // Show welcome logo for new projects
1894
+ console.log();
1895
+ this.printLogo([
1896
+ chalk.bold("Teammates") + chalk.gray(` v${PKG_VERSION}`),
1897
+ chalk.yellow("New project setup"),
1898
+ chalk.gray(process.cwd()),
1899
+ ]);
1900
+ }
1901
+ // Always onboard the user first if USER.md is missing
1902
+ if (this.needsUserSetup(teammatesDir)) {
1903
+ await this.runUserSetup(teammatesDir);
1904
+ }
1905
+ // Team onboarding if .teammates/ was missing
1906
+ if (isNewProject) {
1907
+ const cont = await this.promptTeamOnboarding(adapter, teammatesDir);
1908
+ if (!cont)
1567
1909
  return; // user chose to exit
1568
1910
  }
1569
- // Check if USER.md needs setup — we'll run the interview inside the
1570
- // ChatView after the UI loads (not before).
1571
- const pendingUserInterview = this.needsUserSetup(teammatesDir);
1572
1911
  // Init orchestrator
1573
1912
  this.teammatesDir = teammatesDir;
1574
1913
  this.orchestrator = new Orchestrator({
@@ -1577,26 +1916,38 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1577
1916
  onEvent: (e) => this.handleEvent(e),
1578
1917
  });
1579
1918
  await this.orchestrator.init();
1580
- // Register the agent itself as a mentionable teammate
1581
- const registry = this.orchestrator.getRegistry();
1582
- registry.register({
1583
- name: this.adapterName,
1584
- role: `General-purpose coding agent (${this.adapterName})`,
1585
- soul: "",
1586
- wisdom: "",
1587
- dailyLogs: [],
1588
- weeklyLogs: [],
1589
- ownership: { primary: [], secondary: [] },
1590
- routingKeywords: [],
1591
- cwd: dirname(this.teammatesDir),
1592
- });
1593
- // Add status entry (init() already ran, so we add it manually)
1594
- this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle" });
1919
+ // Register the local user's avatar if alias is configured.
1920
+ // The user's avatar is the entry point for all generic/fallback tasks —
1921
+ // the coding agent is an internal execution engine, not an addressable teammate.
1922
+ const alias = this.readUserAlias(teammatesDir);
1923
+ if (alias) {
1924
+ this.registerUserAvatar(teammatesDir, alias);
1925
+ }
1926
+ else {
1927
+ // No alias yet (solo mode or pre-interview). Register a minimal avatar
1928
+ // under the adapter name so internal tasks (btw, summarize, debug) can execute.
1929
+ const registry = this.orchestrator.getRegistry();
1930
+ registry.register({
1931
+ name: this.adapterName,
1932
+ type: "ai",
1933
+ role: "General-purpose coding agent",
1934
+ soul: "",
1935
+ wisdom: "",
1936
+ dailyLogs: [],
1937
+ weeklyLogs: [],
1938
+ ownership: { primary: [], secondary: [] },
1939
+ routingKeywords: [],
1940
+ cwd: dirname(this.teammatesDir),
1941
+ });
1942
+ this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle", presence: "online" });
1943
+ }
1595
1944
  // Populate roster on the adapter so prompts include team info
1945
+ // Exclude the user avatar and adapter fallback — neither is an addressable teammate
1596
1946
  if ("roster" in this.adapter) {
1597
1947
  const registry = this.orchestrator.getRegistry();
1598
1948
  this.adapter.roster = this.orchestrator
1599
1949
  .listTeammates()
1950
+ .filter((n) => n !== this.adapterName && n !== this.userAlias)
1600
1951
  .map((name) => {
1601
1952
  const t = registry.get(name);
1602
1953
  return { name: t.name, role: t.role, ownership: t.ownership };
@@ -1614,8 +1965,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1614
1965
  borderStyle: (s) => chalk.gray(s),
1615
1966
  colorize: (value) => {
1616
1967
  const validNames = new Set([
1617
- ...this.orchestrator.listTeammates(),
1618
- this.adapterName,
1968
+ ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName),
1969
+ this.selfName,
1619
1970
  "everyone",
1620
1971
  ]);
1621
1972
  return value
@@ -1654,18 +2005,25 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1654
2005
  // ── Detect service statuses ────────────────────────────────────────
1655
2006
  this.serviceStatuses = this.detectServices();
1656
2007
  // ── Build animated banner for ChatView ─────────────────────────────
1657
- const names = this.orchestrator.listTeammates();
2008
+ const names = this.orchestrator
2009
+ .listTeammates()
2010
+ .filter((n) => n !== this.adapterName && n !== this.userAlias);
1658
2011
  const reg = this.orchestrator.getRegistry();
2012
+ const statuses = this.orchestrator.getAllStatuses();
2013
+ const bannerTeammates = [];
2014
+ for (const name of names) {
2015
+ const t = reg.get(name);
2016
+ const p = statuses.get(name)?.presence ?? "online";
2017
+ bannerTeammates.push({ name, role: t?.role ?? "", presence: p });
2018
+ }
1659
2019
  const bannerWidget = new AnimatedBanner({
1660
- adapterName: this.adapterName,
2020
+ displayName: `@${this.selfName}`,
1661
2021
  teammateCount: names.length,
1662
2022
  cwd: process.cwd(),
1663
- teammates: names.map((name) => {
1664
- const t = reg.get(name);
1665
- return { name, role: t?.role ?? "" };
1666
- }),
2023
+ teammates: bannerTeammates,
1667
2024
  services: this.serviceStatuses,
1668
2025
  });
2026
+ this.banner = bannerWidget;
1669
2027
  // ── Create ChatView and Consolonia App ────────────────────────────
1670
2028
  const t = theme();
1671
2029
  this.chatView = new ChatView({
@@ -1688,10 +2046,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1688
2046
  styles[i] = accentStyle;
1689
2047
  }
1690
2048
  }
1691
- // Colorize @mentions only if they reference a valid teammate or the coding agent
2049
+ // Colorize @mentions only if they reference a valid teammate or the user
1692
2050
  const validNames = new Set([
1693
- ...this.orchestrator.listTeammates(),
1694
- this.adapterName,
2051
+ ...this.orchestrator.listTeammates().filter((n) => n !== this.adapterName),
2052
+ this.selfName,
1695
2053
  "everyone",
1696
2054
  ]);
1697
2055
  const mentionPattern = /@(\w+)/g;
@@ -1734,10 +2092,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1734
2092
  progressStyle: { fg: t.progress, italic: true },
1735
2093
  dropdownHighlightStyle: { fg: t.accent },
1736
2094
  dropdownStyle: { fg: t.textMuted },
1737
- footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`)),
2095
+ footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName)),
2096
+ footerRight: tp.muted("? /help "),
1738
2097
  footerStyle: { fg: t.textDim },
1739
2098
  });
1740
- this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`));
2099
+ this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName));
2100
+ this.defaultFooterRight = tp.muted("? /help ");
1741
2101
  // Wire ChatView events for input handling
1742
2102
  this.chatView.on("submit", (rawLine) => {
1743
2103
  this.handleSubmit(rawLine).catch((err) => {
@@ -1763,6 +2123,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1763
2123
  this.escTimer = null;
1764
2124
  }
1765
2125
  this.chatView.setFooter(this.defaultFooter);
2126
+ this.chatView.setFooterRight(this.defaultFooterRight);
1766
2127
  this.refreshView();
1767
2128
  }
1768
2129
  if (this.ctrlcPending) {
@@ -1772,6 +2133,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1772
2133
  this.ctrlcTimer = null;
1773
2134
  }
1774
2135
  this.chatView.setFooter(this.defaultFooter);
2136
+ this.chatView.setFooterRight(this.defaultFooterRight);
1775
2137
  this.refreshView();
1776
2138
  }
1777
2139
  });
@@ -1795,22 +2157,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1795
2157
  }
1796
2158
  this.chatView.inputValue = "";
1797
2159
  this.chatView.setFooter(this.defaultFooter);
2160
+ this.chatView.setFooterRight(this.defaultFooterRight);
1798
2161
  this.pastedTexts.clear();
1799
2162
  this.refreshView();
1800
2163
  }
1801
2164
  else if (this.chatView.inputValue.length > 0) {
1802
- // First ESC with text — show hint in footer, auto-expire after 2s
2165
+ // First ESC with text — show hint in footer right, auto-expire after 2s
1803
2166
  this.escPending = true;
1804
- const termW = process.stdout.columns || 80;
1805
- const hint = "ESC again to clear";
1806
- const pad = Math.max(0, termW - hint.length - 1);
1807
- this.chatView.setFooter(concat(tp.dim(" ".repeat(pad)), tp.muted(hint)));
2167
+ this.chatView.setFooterRight(tp.muted("ESC again to clear "));
1808
2168
  this.refreshView();
1809
2169
  this.escTimer = setTimeout(() => {
1810
2170
  this.escTimer = null;
1811
2171
  if (this.escPending) {
1812
2172
  this.escPending = false;
1813
2173
  this.chatView.setFooter(this.defaultFooter);
2174
+ this.chatView.setFooterRight(this.defaultFooterRight);
1814
2175
  this.refreshView();
1815
2176
  }
1816
2177
  }, 2000);
@@ -1828,29 +2189,31 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1828
2189
  this.ctrlcTimer = null;
1829
2190
  }
1830
2191
  this.chatView.setFooter(this.defaultFooter);
2192
+ this.chatView.setFooterRight(this.defaultFooterRight);
1831
2193
  if (this.app)
1832
2194
  this.app.stop();
1833
2195
  this.orchestrator.shutdown().then(() => process.exit(0));
1834
2196
  return;
1835
2197
  }
1836
- // First Ctrl+C — show hint in footer, auto-expire after 2s
2198
+ // First Ctrl+C — show hint in footer right, auto-expire after 2s
1837
2199
  this.ctrlcPending = true;
1838
- const termW = process.stdout.columns || 80;
1839
- const hint = "Ctrl+C again to exit";
1840
- const pad = Math.max(0, termW - hint.length - 1);
1841
- this.chatView.setFooter(concat(tp.dim(" ".repeat(pad)), tp.muted(hint)));
2200
+ this.chatView.setFooterRight(tp.muted("Ctrl+C again to exit "));
1842
2201
  this.refreshView();
1843
2202
  this.ctrlcTimer = setTimeout(() => {
1844
2203
  this.ctrlcTimer = null;
1845
2204
  if (this.ctrlcPending) {
1846
2205
  this.ctrlcPending = false;
1847
2206
  this.chatView.setFooter(this.defaultFooter);
2207
+ this.chatView.setFooterRight(this.defaultFooterRight);
1848
2208
  this.refreshView();
1849
2209
  }
1850
2210
  }, 2000);
1851
2211
  });
1852
2212
  this.chatView.on("action", (id) => {
1853
- if (id === "copy") {
2213
+ if (id.startsWith("copy-cmd:")) {
2214
+ this.doCopy(id.slice("copy-cmd:".length));
2215
+ }
2216
+ else if (id === "copy") {
1854
2217
  this.doCopy(this.lastCleanedOutput || undefined);
1855
2218
  }
1856
2219
  else if (id.startsWith("retro-approve-") ||
@@ -1899,15 +2262,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1899
2262
  // Start the banner animation after the first frame renders.
1900
2263
  bannerWidget.onDirty = () => this.app?.refresh();
1901
2264
  const runPromise = this.app.run();
1902
- // Hold the banner animation before commands if we need to run the interview
1903
- if (pendingUserInterview) {
1904
- bannerWidget.hold();
1905
- }
1906
2265
  bannerWidget.start();
1907
- // Run user interview inside the ChatView if USER.md needs setup
1908
- if (pendingUserInterview) {
1909
- this.startUserInterview(teammatesDir, bannerWidget);
1910
- }
1911
2266
  await runPromise;
1912
2267
  }
1913
2268
  /**
@@ -1930,7 +2285,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1930
2285
  const fileName = trimmed.split(/[/\\]/).pop() || trimmed;
1931
2286
  const n = ++this.pasteCounter;
1932
2287
  this.pastedTexts.set(n, `[Image: source: ${trimmed}]`);
1933
- const placeholder = `[Image ${fileName}]`;
2288
+ const placeholder = `[Image ${fileName}] `;
1934
2289
  const newVal = current.slice(0, idx) +
1935
2290
  placeholder +
1936
2291
  current.slice(idx + clean.length);
@@ -1967,9 +2322,28 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1967
2322
  }
1968
2323
  /** Handle line submission from ChatView. */
1969
2324
  async handleSubmit(rawLine) {
2325
+ // If an inline ask is pending, resolve it instead of normal processing
2326
+ if (this._pendingAsk) {
2327
+ const resolve = this._pendingAsk;
2328
+ this._pendingAsk = null;
2329
+ resolve(rawLine);
2330
+ return;
2331
+ }
1970
2332
  this.clearWordwheel();
1971
2333
  this.wordwheelItems = [];
1972
2334
  this.wordwheelIndex = -1;
2335
+ // Resolve @mentions from the raw input BEFORE paste expansion.
2336
+ // This prevents @mentions inside pasted/expanded text from being picked up.
2337
+ const allNames = this.orchestrator.listTeammates();
2338
+ const preMentionRegex = /@(\S+)/g;
2339
+ let pm;
2340
+ const preMentions = [];
2341
+ while ((pm = preMentionRegex.exec(rawLine)) !== null) {
2342
+ const name = pm[1];
2343
+ if (allNames.includes(name) && !preMentions.includes(name)) {
2344
+ preMentions.push(name);
2345
+ }
2346
+ }
1973
2347
  // Expand paste placeholders with actual content
1974
2348
  let input = rawLine.replace(/\[Pasted text #(\d+) \+\d+ lines, [\d.]+KB\]\s*/g, (_match, num) => {
1975
2349
  const n = parseInt(num, 10);
@@ -2050,10 +2424,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2050
2424
  this.refreshView();
2051
2425
  return;
2052
2426
  }
2053
- // Everything else gets queued
2054
- this.conversationHistory.push({ role: "user", text: input });
2427
+ // Everything else gets queued.
2428
+ // Pass pre-resolved mentions so @mentions inside expanded paste text are ignored.
2429
+ this.conversationHistory.push({ role: this.selfName, text: input });
2055
2430
  this.printUserMessage(input);
2056
- this.queueTask(input);
2431
+ this.queueTask(input, preMentions);
2057
2432
  this.refreshView();
2058
2433
  }
2059
2434
  printBanner(teammates) {
@@ -2061,7 +2436,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2061
2436
  const termWidth = process.stdout.columns || 100;
2062
2437
  this.feedLine();
2063
2438
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2064
- this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2439
+ this.feedLine(concat(tp.text(` @${this.selfName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2065
2440
  this.feedLine(` ${process.cwd()}`);
2066
2441
  // Service status rows
2067
2442
  for (const svc of this.serviceStatuses) {
@@ -2077,12 +2452,15 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2077
2452
  : `missing — /configure ${svc.name.toLowerCase()}`;
2078
2453
  this.feedLine(concat(tp.text(" "), color(icon), color(svc.name), tp.muted(` ${label}`)));
2079
2454
  }
2080
- // Roster
2455
+ // Roster (with presence indicators)
2081
2456
  this.feedLine();
2457
+ const statuses = this.orchestrator.getAllStatuses();
2082
2458
  for (const name of teammates) {
2083
2459
  const t = registry.get(name);
2084
2460
  if (t) {
2085
- this.feedLine(concat(tp.muted(" "), tp.accent(`● @${name.padEnd(14)}`), tp.muted(t.role)));
2461
+ const p = statuses.get(name)?.presence ?? "online";
2462
+ const dot = p === "online" ? tp.success("●") : p === "reachable" ? tp.warning("●") : tp.error("●");
2463
+ this.feedLine(concat(tp.text(" "), dot, tp.accent(` @${name.padEnd(14)}`), tp.muted(t.role)));
2086
2464
  }
2087
2465
  }
2088
2466
  this.feedLine();
@@ -2202,43 +2580,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2202
2580
  this.feedLine(tp.warning(" GitHub CLI is not installed."));
2203
2581
  this.feedLine();
2204
2582
  const plat = process.platform;
2205
- let installCmd;
2206
- let installLabel;
2583
+ this.feedLine(tp.text(" Run this in another terminal:"));
2207
2584
  if (plat === "win32") {
2208
- installCmd = "winget install --id GitHub.cli";
2209
- installLabel = "winget install --id GitHub.cli";
2585
+ this.feedCommand("winget install --id GitHub.cli");
2210
2586
  }
2211
2587
  else if (plat === "darwin") {
2212
- installCmd = "brew install gh";
2213
- installLabel = "brew install gh";
2588
+ this.feedCommand("brew install gh");
2214
2589
  }
2215
2590
  else {
2216
- installCmd = "sudo apt install gh";
2217
- installLabel = "sudo apt install gh (or see https://cli.github.com)";
2591
+ this.feedCommand("sudo apt install gh");
2592
+ this.feedLine(tp.muted(" (or see https://cli.github.com)"));
2218
2593
  }
2219
- this.feedLine(tp.text(` Install: ${installLabel}`));
2220
2594
  this.feedLine();
2221
- // Ask user
2222
- const answer = await this.askInput("Run install command? [Y/n] ");
2595
+ const answer = await this.askInline("Press Enter when done (or n to skip)");
2223
2596
  if (answer.toLowerCase() === "n") {
2224
- this.feedLine(tp.muted(" Skipped. Install manually and re-run /configure github"));
2225
- this.refreshView();
2226
- return;
2227
- }
2228
- // Spawn install in a visible subprocess
2229
- this.feedLine(tp.muted(` Running: ${installCmd}`));
2230
- this.refreshView();
2231
- const installSuccess = await new Promise((res) => {
2232
- const parts = installCmd.split(" ");
2233
- const child = spawn(parts[0], parts.slice(1), {
2234
- stdio: "inherit",
2235
- shell: true,
2236
- });
2237
- child.on("error", () => res(false));
2238
- child.on("exit", (code) => res(code === 0));
2239
- });
2240
- if (!installSuccess) {
2241
- this.feedLine(tp.error(" Install failed. Please install manually from https://cli.github.com"));
2597
+ this.feedLine(tp.muted(" Skipped. Run /configure github when ready."));
2242
2598
  this.refreshView();
2243
2599
  return;
2244
2600
  }
@@ -2249,7 +2605,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2249
2605
  this.feedLine(tp.success(" ✓ GitHub CLI installed"));
2250
2606
  }
2251
2607
  catch {
2252
- this.feedLine(tp.error(" GitHub CLI still not found after install. You may need to restart your terminal."));
2608
+ this.feedLine(tp.error(" GitHub CLI still not found. You may need to restart your terminal."));
2253
2609
  this.refreshView();
2254
2610
  return;
2255
2611
  }
@@ -2268,31 +2624,19 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2268
2624
  // not authenticated
2269
2625
  }
2270
2626
  if (!authed) {
2271
- this.feedLine(tp.muted(" Authentication needed — this will open your browser for GitHub OAuth."));
2272
2627
  this.feedLine();
2273
- const answer = await this.askInput("Start authentication? [Y/n] ");
2628
+ this.feedLine(tp.text(" Run this in another terminal to authenticate:"));
2629
+ this.feedCommand("gh auth login --web --git-protocol https");
2630
+ this.feedLine();
2631
+ this.feedLine(tp.muted(" This will open your browser for GitHub OAuth."));
2632
+ this.feedLine();
2633
+ const answer = await this.askInline("Press Enter when done (or n to skip)");
2274
2634
  if (answer.toLowerCase() === "n") {
2275
2635
  this.feedLine(tp.muted(" Skipped. Run /configure github when ready."));
2276
2636
  this.refreshView();
2277
2637
  this.updateServiceStatus("GitHub", "not-configured");
2278
2638
  return;
2279
2639
  }
2280
- this.feedLine(tp.muted(" Starting auth flow..."));
2281
- this.refreshView();
2282
- const authSuccess = await new Promise((res) => {
2283
- const child = spawn("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
2284
- stdio: "inherit",
2285
- shell: true,
2286
- });
2287
- child.on("error", () => res(false));
2288
- child.on("exit", (code) => res(code === 0));
2289
- });
2290
- if (!authSuccess) {
2291
- this.feedLine(tp.error(" Authentication failed. Try again with /configure github"));
2292
- this.refreshView();
2293
- this.updateServiceStatus("GitHub", "not-configured");
2294
- return;
2295
- }
2296
2640
  // Verify
2297
2641
  try {
2298
2642
  execSync("gh auth status", { stdio: "pipe" });
@@ -2320,8 +2664,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2320
2664
  }
2321
2665
  updateServiceStatus(name, status) {
2322
2666
  const svc = this.serviceStatuses.find((s) => s.name === name);
2323
- if (svc)
2667
+ if (svc) {
2324
2668
  svc.status = status;
2669
+ if (this.banner) {
2670
+ this.banner.updateServices(this.serviceStatuses);
2671
+ this.refreshView();
2672
+ }
2673
+ }
2325
2674
  }
2326
2675
  registerCommands() {
2327
2676
  const cmds = [
@@ -2471,6 +2820,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2471
2820
  if (this.activeTasks.size === 0) {
2472
2821
  this.stopStatusAnimation();
2473
2822
  }
2823
+ // Suppress display for internal summarization tasks
2824
+ const activeEntry = this.agentActive.get(event.result.teammate);
2825
+ if (activeEntry?.type === "summarize")
2826
+ break;
2474
2827
  if (!this.chatView)
2475
2828
  this.input.deactivateAndErase();
2476
2829
  const raw = event.result.rawOutput ?? "";
@@ -2484,17 +2837,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2484
2837
  const sizeKB = cleaned ? Buffer.byteLength(cleaned, "utf-8") / 1024 : 0;
2485
2838
  // Header: "teammate: subject"
2486
2839
  const subject = event.result.summary || "Task completed";
2487
- this.feedLine(concat(tp.accent(`${event.result.teammate}: `), tp.text(subject)));
2840
+ const displayTeammate = event.result.teammate === this.adapterName ? this.selfName : event.result.teammate;
2841
+ this.feedLine(concat(tp.accent(`${displayTeammate}: `), tp.text(subject)));
2488
2842
  this.lastCleanedOutput = cleaned;
2489
- if (sizeKB > 5) {
2490
- const tmpFile = join(tmpdir(), `teammates-${event.result.teammate}-${Date.now()}.md`);
2491
- writeFileSync(tmpFile, cleaned, "utf-8");
2492
- this.feedLine(tp.muted(` ${"─".repeat(40)}`));
2493
- this.feedLine(tp.warning(` ⚠ Response is ${sizeKB.toFixed(1)}KB — saved to temp file:`));
2494
- this.feedLine(tp.muted(` ${tmpFile}`));
2495
- this.feedLine(tp.muted(` ${"─".repeat(40)}`));
2496
- }
2497
- else if (cleaned) {
2843
+ if (cleaned) {
2498
2844
  this.feedMarkdown(cleaned);
2499
2845
  }
2500
2846
  else {
@@ -2562,7 +2908,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2562
2908
  this.stopStatusAnimation();
2563
2909
  if (!this.chatView)
2564
2910
  this.input.deactivateAndErase();
2565
- this.feedLine(tp.error(` ✖ ${event.teammate}: ${event.error}`));
2911
+ const displayErr = event.teammate === this.adapterName ? this.selfName : event.teammate;
2912
+ this.feedLine(tp.error(` ✖ ${displayErr}: ${event.error}`));
2566
2913
  this.showPrompt();
2567
2914
  break;
2568
2915
  }
@@ -2573,16 +2920,36 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2573
2920
  this.feedLine();
2574
2921
  this.feedLine(tp.bold(" Status"));
2575
2922
  this.feedLine(tp.muted(` ${"─".repeat(50)}`));
2923
+ // Show user avatar first if present
2924
+ if (this.userAlias) {
2925
+ const userStatus = statuses.get(this.userAlias);
2926
+ 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}`));
2931
+ this.feedLine();
2932
+ }
2933
+ }
2576
2934
  for (const [name, status] of statuses) {
2935
+ // Skip the user avatar (shown above) and adapter fallback (not addressable)
2936
+ if (name === this.adapterName || name === this.userAlias)
2937
+ continue;
2577
2938
  const t = registry.get(name);
2578
2939
  const active = this.agentActive.get(name);
2579
2940
  const queued = this.taskQueue.filter((e) => e.teammate === name);
2941
+ // Presence indicator: ● green=online, ● red=offline, ● yellow=reachable
2942
+ const presenceIcon = status.presence === "online"
2943
+ ? tp.success("●")
2944
+ : status.presence === "reachable"
2945
+ ? tp.warning("●")
2946
+ : tp.error("●");
2580
2947
  // Teammate name + state
2581
2948
  const stateLabel = active ? "working" : status.state;
2582
2949
  const stateColor = stateLabel === "working"
2583
2950
  ? tp.info(` (${stateLabel})`)
2584
2951
  : tp.muted(` (${stateLabel})`);
2585
- this.feedLine(concat(tp.accent(` @${name}`), stateColor));
2952
+ this.feedLine(concat(presenceIcon, tp.accent(` @${name}`), stateColor));
2586
2953
  // Role
2587
2954
  if (t) {
2588
2955
  this.feedLine(tp.muted(` ${t.role}`));
@@ -2620,7 +2987,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2620
2987
  // Pick all teammates with debug files, queue one analysis per teammate
2621
2988
  const names = [];
2622
2989
  for (const [name] of this.lastDebugFiles) {
2623
- if (name !== this.adapterName)
2990
+ if (name !== this.selfName)
2624
2991
  names.push(name);
2625
2992
  }
2626
2993
  if (names.length === 0) {
@@ -2685,7 +3052,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2685
3052
  this.refreshView();
2686
3053
  this.taskQueue.push({
2687
3054
  type: "debug",
2688
- teammate: this.adapterName,
3055
+ teammate: this.selfName,
2689
3056
  task: analysisPrompt,
2690
3057
  });
2691
3058
  this.kickDrain();
@@ -2703,7 +3070,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2703
3070
  return;
2704
3071
  }
2705
3072
  const removed = this.taskQueue.splice(n - 1, 1)[0];
2706
- this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${removed.teammate}`), tp.muted(" "), tp.text(removed.task.slice(0, 60))));
3073
+ const cancelDisplay = removed.teammate === this.adapterName ? this.selfName : removed.teammate;
3074
+ this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
2707
3075
  this.refreshView();
2708
3076
  }
2709
3077
  /** Drain tasks for a single agent — runs in parallel with other agents. */
@@ -2719,6 +3087,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2719
3087
  if (entry.type === "compact") {
2720
3088
  await this.runCompact(entry.teammate);
2721
3089
  }
3090
+ else if (entry.type === "summarize") {
3091
+ // Internal housekeeping — summarize older conversation history
3092
+ const result = await this.orchestrator.assign({
3093
+ teammate: entry.teammate,
3094
+ task: entry.task,
3095
+ });
3096
+ // Extract the summary from the agent's output (strip protocol artifacts)
3097
+ const raw = result.rawOutput ?? "";
3098
+ this.conversationSummary = raw
3099
+ .replace(/^TO:\s*\S+\s*\n/im, "")
3100
+ .replace(/^#\s+.+\n*/m, "")
3101
+ .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
3102
+ .trim();
3103
+ }
2722
3104
  else {
2723
3105
  // btw and debug tasks skip conversation context (not part of main thread)
2724
3106
  const extraContext = entry.type === "btw" || entry.type === "debug"
@@ -2736,6 +3118,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2736
3118
  // btw and debug results are not stored in conversation history
2737
3119
  if (entry.type !== "btw" && entry.type !== "debug") {
2738
3120
  this.storeResult(result);
3121
+ // Check if older history needs summarizing
3122
+ this.maybeQueueSummarization();
2739
3123
  }
2740
3124
  if (entry.type === "retro") {
2741
3125
  this.handleRetroResult(result);
@@ -2750,7 +3134,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2750
3134
  if (this.activeTasks.size === 0)
2751
3135
  this.stopStatusAnimation();
2752
3136
  const msg = err?.message ?? String(err);
2753
- this.feedLine(tp.error(` ✖ @${agent}: ${msg}`));
3137
+ const displayAgent = agent === this.adapterName ? this.selfName : agent;
3138
+ this.feedLine(tp.error(` ✖ @${displayAgent}: ${msg}`));
2754
3139
  this.refreshView();
2755
3140
  }
2756
3141
  this.agentActive.delete(agent);
@@ -2885,11 +3270,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2885
3270
  // Copy framework files so the agent has TEMPLATE.md etc. available
2886
3271
  await copyTemplateFiles(teammatesDir);
2887
3272
  // Queue a single adaptation task that handles all teammates
2888
- this.feedLine(tp.muted(` Queuing ${this.adapterName} to scan this project and adapt the team...`));
3273
+ this.feedLine(tp.muted(" Queuing agent to scan this project and adapt the team..."));
2889
3274
  const prompt = await buildImportAdaptationPrompt(teammatesDir, allTeammates, sourceDir);
2890
3275
  this.taskQueue.push({
2891
3276
  type: "agent",
2892
- teammate: this.adapterName,
3277
+ teammate: this.selfName,
2893
3278
  task: prompt,
2894
3279
  });
2895
3280
  this.kickDrain();
@@ -2920,6 +3305,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2920
3305
  }
2921
3306
  async cmdClear() {
2922
3307
  this.conversationHistory.length = 0;
3308
+ this.conversationSummary = "";
2923
3309
  this.lastResult = null;
2924
3310
  this.lastResults.clear();
2925
3311
  this.taskQueue.length = 0;
@@ -2948,9 +3334,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2948
3334
  return;
2949
3335
  const registry = this.orchestrator.getRegistry();
2950
3336
  // Update adapter roster so prompts include the new teammates
3337
+ // Exclude the user avatar and adapter fallback — neither is an addressable teammate
2951
3338
  if ("roster" in this.adapter) {
2952
3339
  this.adapter.roster = this.orchestrator
2953
3340
  .listTeammates()
3341
+ .filter((n) => n !== this.adapterName && n !== this.userAlias)
2954
3342
  .map((name) => {
2955
3343
  const t = registry.get(name);
2956
3344
  return { name: t.name, role: t.role, ownership: t.ownership };
@@ -2972,7 +3360,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2972
3360
  const arg = argsStr.trim().replace(/^@/, "");
2973
3361
  const allTeammates = this.orchestrator
2974
3362
  .listTeammates()
2975
- .filter((n) => n !== this.adapterName);
3363
+ .filter((n) => n !== this.selfName && n !== this.adapterName);
2976
3364
  const names = !arg || arg === "everyone" ? allTeammates : [arg];
2977
3365
  // Validate all names first
2978
3366
  const valid = [];
@@ -3043,7 +3431,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3043
3431
  if (spinner)
3044
3432
  spinner.succeed(`${name}: ${parts.join(", ")}`);
3045
3433
  if (this.chatView)
3046
- this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
3434
+ this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
3047
3435
  }
3048
3436
  if (this.chatView)
3049
3437
  this.chatView.setProgress(null);
@@ -3065,7 +3453,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3065
3453
  syncSpinner.succeed(`${name}: index synced`);
3066
3454
  if (this.chatView) {
3067
3455
  this.chatView.setProgress(null);
3068
- this.feedLine(tp.success(` ✔ ${name}: index synced`));
3456
+ this.feedLine(tp.success(` ✔ ${name}: index synced`));
3069
3457
  }
3070
3458
  }
3071
3459
  catch {
@@ -3095,7 +3483,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3095
3483
  spinner.fail(`${name}: ${msg}`);
3096
3484
  if (this.chatView) {
3097
3485
  this.chatView.setProgress(null);
3098
- this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3486
+ this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
3099
3487
  }
3100
3488
  }
3101
3489
  this.refreshView();
@@ -3105,7 +3493,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3105
3493
  // Resolve target list
3106
3494
  const allTeammates = this.orchestrator
3107
3495
  .listTeammates()
3108
- .filter((n) => n !== this.adapterName);
3496
+ .filter((n) => n !== this.selfName && n !== this.adapterName);
3109
3497
  let targets;
3110
3498
  if (arg === "everyone") {
3111
3499
  targets = allTeammates;
@@ -3218,7 +3606,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3218
3606
  }
3219
3607
  const teammates = this.orchestrator
3220
3608
  .listTeammates()
3221
- .filter((n) => n !== this.adapterName);
3609
+ .filter((n) => n !== this.selfName && n !== this.adapterName);
3222
3610
  if (teammates.length === 0)
3223
3611
  return;
3224
3612
  // 1. Check each teammate for stale daily logs (older than 7 days)
@@ -3303,7 +3691,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3303
3691
  child.stdin?.end();
3304
3692
  // Show brief "Copied" message in the progress area
3305
3693
  if (this.chatView) {
3306
- this.chatView.setProgress(concat(tp.success("✔ "), tp.muted("Copied to clipboard")));
3694
+ this.chatView.setProgress(concat(tp.success("✔ "), tp.muted("Copied to clipboard")));
3307
3695
  this.refreshView();
3308
3696
  setTimeout(() => {
3309
3697
  this.chatView.setProgress(null);
@@ -3313,7 +3701,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3313
3701
  }
3314
3702
  catch {
3315
3703
  if (this.chatView) {
3316
- this.chatView.setProgress(concat(tp.error("✖ "), tp.muted("Failed to copy")));
3704
+ this.chatView.setProgress(concat(tp.error("✖ "), tp.muted("Failed to copy")));
3317
3705
  this.refreshView();
3318
3706
  setTimeout(() => {
3319
3707
  this.chatView.setProgress(null);
@@ -3322,6 +3710,19 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3322
3710
  }
3323
3711
  }
3324
3712
  }
3713
+ /**
3714
+ * Feed a command line with a clickable [copy] button.
3715
+ * Renders as: ` command text [copy]`
3716
+ */
3717
+ feedCommand(command) {
3718
+ if (!this.chatView) {
3719
+ this.feedLine(tp.accent(` ${command}`));
3720
+ return;
3721
+ }
3722
+ const normal = concat(tp.accent(` ${command} `), tp.muted("[copy]"));
3723
+ const hover = concat(tp.accent(` ${command} `), tp.accent("[copy]"));
3724
+ this.chatView.appendAction(`copy-cmd:${command}`, normal, hover);
3725
+ }
3325
3726
  async cmdHelp() {
3326
3727
  this.feedLine();
3327
3728
  this.feedLine(tp.bold(" Commands"));
@@ -3373,10 +3774,10 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3373
3774
  this.refreshView();
3374
3775
  return;
3375
3776
  }
3376
- // Has args — queue a task to the coding agent to apply the change
3777
+ // Has args — queue a task to apply the change
3377
3778
  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.`;
3378
- this.taskQueue.push({ type: "agent", teammate: this.adapterName, task });
3379
- this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.adapterName}`)));
3779
+ this.taskQueue.push({ type: "agent", teammate: this.selfName, task });
3780
+ this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.selfName}`)));
3380
3781
  this.feedLine();
3381
3782
  this.refreshView();
3382
3783
  this.kickDrain();
@@ -3390,10 +3791,10 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3390
3791
  }
3391
3792
  this.taskQueue.push({
3392
3793
  type: "btw",
3393
- teammate: this.adapterName,
3794
+ teammate: this.selfName,
3394
3795
  task: question,
3395
3796
  });
3396
- this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.adapterName}`)));
3797
+ this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.selfName}`)));
3397
3798
  this.feedLine();
3398
3799
  this.refreshView();
3399
3800
  this.kickDrain();
@@ -3422,9 +3823,9 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3422
3823
  row("textDim", t.textDim, "─── separator ───");
3423
3824
  this.feedLine();
3424
3825
  // Status
3425
- row("success", t.success, "✔ Task completed");
3426
- row("warning", t.warning, "⚠ Pending handoff");
3427
- row("error", t.error, "✖ Something went wrong");
3826
+ row("success", t.success, "✔ Task completed");
3827
+ row("warning", t.warning, "⚠ Pending handoff");
3828
+ row("error", t.error, "✖ Something went wrong");
3428
3829
  row("info", t.info, "⠋ Working on task...");
3429
3830
  this.feedLine();
3430
3831
  // Interactive
@@ -3492,9 +3893,9 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3492
3893
  "",
3493
3894
  "| Language | Status |",
3494
3895
  "|------------|---------|",
3495
- "| JavaScript | ✔ Ready |",
3496
- "| Python | ✔ Ready |",
3497
- "| C# | ✔ Ready |",
3896
+ "| JavaScript | ✔ Ready |",
3897
+ "| Python | ✔ Ready |",
3898
+ "| C# | ✔ Ready |",
3498
3899
  "",
3499
3900
  "---",
3500
3901
  ].join("\n");