@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.
- package/README.md +36 -4
- package/dist/adapter.d.ts +13 -3
- package/dist/adapter.js +48 -11
- package/dist/adapter.test.js +1 -0
- package/dist/adapters/cli-proxy.d.ts +3 -1
- package/dist/adapters/cli-proxy.js +19 -4
- package/dist/adapters/copilot.d.ts +3 -1
- package/dist/adapters/copilot.js +16 -2
- package/dist/adapters/echo.d.ts +3 -1
- package/dist/adapters/echo.js +2 -2
- package/dist/adapters/echo.test.js +1 -0
- package/dist/banner.d.ts +6 -1
- package/dist/banner.js +18 -3
- package/dist/cli-args.js +0 -1
- package/dist/cli.js +914 -346
- package/dist/console/startup.d.ts +2 -1
- package/dist/console/startup.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +18 -13
- package/dist/orchestrator.test.js +2 -1
- package/dist/personas.d.ts +42 -0
- package/dist/personas.js +108 -0
- package/dist/registry.js +7 -0
- package/dist/registry.test.js +1 -0
- package/dist/types.d.ts +8 -0
- package/package.json +4 -3
- package/personas/architect.md +91 -0
- package/personas/backend.md +93 -0
- package/personas/data-engineer.md +92 -0
- package/personas/designer.md +92 -0
- package/personas/devops.md +93 -0
- package/personas/frontend.md +94 -0
- package/personas/ml-ai.md +96 -0
- package/personas/mobile.md +93 -0
- package/personas/performance.md +92 -0
- package/personas/pm.md +89 -0
- package/personas/qa.md +92 -0
- package/personas/security.md +92 -0
- package/personas/sre.md +93 -0
- package/personas/swe.md +88 -0
- 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,
|
|
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,
|
|
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
|
|
209
|
+
// Queue the summarization task through the user's agent
|
|
107
210
|
this.taskQueue.push({
|
|
108
211
|
type: "summarize",
|
|
109
|
-
teammate: this.
|
|
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
|
-
|
|
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) -
|
|
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} ${
|
|
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(
|
|
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
|
|
303
|
-
const label =
|
|
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(` ✖
|
|
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
|
-
//
|
|
869
|
-
|
|
870
|
-
let
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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.
|
|
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.
|
|
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
|
|
928
|
-
*
|
|
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
|
|
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("
|
|
947
|
-
chalk.gray(" —
|
|
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("
|
|
1078
|
+
console.log(chalk.cyan(" 4") +
|
|
953
1079
|
chalk.gray(") ") +
|
|
954
1080
|
chalk.white("Solo mode") +
|
|
955
|
-
chalk.gray(
|
|
956
|
-
console.log(chalk.cyan("
|
|
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 === "
|
|
1091
|
+
if (choice === "5") {
|
|
965
1092
|
console.log(chalk.gray(" Goodbye."));
|
|
966
|
-
return
|
|
1093
|
+
return false;
|
|
967
1094
|
}
|
|
968
|
-
if (choice === "
|
|
969
|
-
|
|
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
|
|
1099
|
+
return true;
|
|
976
1100
|
}
|
|
977
|
-
if (choice === "
|
|
978
|
-
// Import from another project
|
|
979
|
-
await mkdir(teammatesDir, { recursive: true });
|
|
1101
|
+
if (choice === "3") {
|
|
980
1102
|
await this.runImport(cwd);
|
|
981
|
-
return
|
|
1103
|
+
return true;
|
|
982
1104
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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(
|
|
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.
|
|
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(" ✔
|
|
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(
|
|
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.
|
|
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(" ✔
|
|
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
|
-
*
|
|
1226
|
-
*
|
|
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
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
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: `@${
|
|
1995
|
+
label: `@${display}`,
|
|
1479
1996
|
description: t?.role ?? "",
|
|
1480
|
-
completion: `${before}@${
|
|
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
|
-
//
|
|
2133
|
+
// Detect whether this is a brand-new project (no .teammates/ at all)
|
|
2134
|
+
const isNewProject = !teammatesDir;
|
|
1617
2135
|
if (!teammatesDir) {
|
|
1618
|
-
teammatesDir =
|
|
1619
|
-
|
|
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
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
|
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
|
-
|
|
2271
|
+
displayName: `@${this.adapterName}`,
|
|
1714
2272
|
teammateCount: names.length,
|
|
1715
2273
|
cwd: process.cwd(),
|
|
1716
|
-
teammates:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
2259
|
-
let installLabel;
|
|
2841
|
+
this.feedLine(tp.text(" Run this in another terminal:"));
|
|
2260
2842
|
if (plat === "win32") {
|
|
2261
|
-
|
|
2262
|
-
installLabel = "winget install --id GitHub.cli";
|
|
2843
|
+
this.feedCommand("winget install --id GitHub.cli");
|
|
2263
2844
|
}
|
|
2264
2845
|
else if (plat === "darwin") {
|
|
2265
|
-
|
|
2266
|
-
installLabel = "brew install gh";
|
|
2846
|
+
this.feedCommand("brew install gh");
|
|
2267
2847
|
}
|
|
2268
2848
|
else {
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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(`
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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(` ✔
|
|
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(` ✔
|
|
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(` ✖
|
|
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("✔
|
|
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("✖
|
|
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
|
|
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.
|
|
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.
|
|
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, "✔
|
|
3492
|
-
row("warning", t.warning, "⚠
|
|
3493
|
-
row("error", t.error, "✖
|
|
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 | ✔
|
|
3562
|
-
"| Python | ✔
|
|
3563
|
-
"| C# | ✔
|
|
4129
|
+
"| JavaScript | ✔ Ready |",
|
|
4130
|
+
"| Python | ✔ Ready |",
|
|
4131
|
+
"| C# | ✔ Ready |",
|
|
3564
4132
|
"",
|
|
3565
4133
|
"---",
|
|
3566
4134
|
].join("\n");
|