@teammates/cli 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/adapter.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface AgentAdapter {
24
24
  * Falls back to startSession if not implemented.
25
25
  */
26
26
  resumeSession?(teammate: TeammateConfig, sessionId: string): Promise<string>;
27
+ /** Get the session file path for a teammate (if session is active). */
28
+ getSessionFile?(teammateName: string): string | undefined;
27
29
  /** Clean up a session. */
28
30
  destroySession?(sessionId: string): Promise<void>;
29
31
  /**
@@ -70,6 +70,7 @@ export declare class CliProxyAdapter implements AgentAdapter {
70
70
  startSession(teammate: TeammateConfig): Promise<string>;
71
71
  executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
72
72
  routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
73
+ getSessionFile(teammateName: string): string | undefined;
73
74
  destroySession(_sessionId: string): Promise<void>;
74
75
  /**
75
76
  * Spawn the agent, stream its output live, and capture it.
@@ -114,8 +114,9 @@ export class CliProxyAdapter {
114
114
  // Create session file inside .teammates/.tmp so sandboxed agents can access it
115
115
  if (!this.sessionsDir) {
116
116
  const tmpBase = join(teammate.cwd ?? process.cwd(), ".teammates", ".tmp");
117
- this.sessionsDir = join(tmpBase, "sessions");
118
- await mkdir(this.sessionsDir, { recursive: true });
117
+ const dir = join(tmpBase, "sessions");
118
+ await mkdir(dir, { recursive: true });
119
+ this.sessionsDir = dir;
119
120
  // Ensure .tmp is gitignored
120
121
  const gitignorePath = join(tmpBase, "..", ".gitignore");
121
122
  const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
@@ -269,6 +270,9 @@ export class CliProxyAdapter {
269
270
  await unlink(promptFile).catch(() => { });
270
271
  }
271
272
  }
273
+ getSessionFile(teammateName) {
274
+ return this.sessionFiles.get(teammateName);
275
+ }
272
276
  async destroySession(_sessionId) {
273
277
  // Clean up any leaked temp prompt files
274
278
  for (const file of this.pendingTempFiles) {
@@ -42,6 +42,7 @@ export declare class CopilotAdapter implements AgentAdapter {
42
42
  startSession(teammate: TeammateConfig): Promise<string>;
43
43
  executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
44
44
  routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
45
+ getSessionFile(teammateName: string): string | undefined;
45
46
  destroySession(_sessionId: string): Promise<void>;
46
47
  /**
47
48
  * Ensure the CopilotClient is started.
@@ -39,8 +39,9 @@ export class CopilotAdapter {
39
39
  // Create session file inside .teammates/.tmp so the agent can access it
40
40
  if (!this.sessionsDir) {
41
41
  const tmpBase = join(teammate.cwd ?? process.cwd(), ".teammates", ".tmp");
42
- this.sessionsDir = join(tmpBase, "sessions");
43
- await mkdir(this.sessionsDir, { recursive: true });
42
+ const dir = join(tmpBase, "sessions");
43
+ await mkdir(dir, { recursive: true });
44
+ this.sessionsDir = dir;
44
45
  const gitignorePath = join(tmpBase, "..", ".gitignore");
45
46
  const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
46
47
  if (!existing.includes(".tmp/")) {
@@ -173,6 +174,9 @@ export class CopilotAdapter {
173
174
  await session.disconnect().catch(() => { });
174
175
  }
175
176
  }
177
+ getSessionFile(teammateName) {
178
+ return this.sessionFiles.get(teammateName);
179
+ }
176
180
  async destroySession(_sessionId) {
177
181
  // Disconnect all sessions
178
182
  for (const [, session] of this.sessions) {
package/dist/cli.js CHANGED
@@ -8,10 +8,10 @@
8
8
  * teammates --dir <path> Override .teammates/ location
9
9
  */
10
10
  import { spawn as cpSpawn, exec as execCb, execSync, } from "node:child_process";
11
- import { readFileSync, writeFileSync } from "node:fs";
11
+ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
13
13
  import { tmpdir } from "node:os";
14
- import { join, resolve } from "node:path";
14
+ import { dirname, join, resolve } from "node:path";
15
15
  import { createInterface } from "node:readline";
16
16
  import { promisify } from "node:util";
17
17
  const execAsync = promisify(execCb);
@@ -24,7 +24,7 @@ import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils
24
24
  import { compactEpisodic } from "./compact.js";
25
25
  import { PromptInput } from "./console/prompt-input.js";
26
26
  import { buildTitle } from "./console/startup.js";
27
- import { buildAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
27
+ import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
28
28
  import { Orchestrator } from "./orchestrator.js";
29
29
  import { colorToHex, theme } from "./theme.js";
30
30
  // ─── Version ─────────────────────────────────────────────────────────
@@ -465,6 +465,28 @@ class TeammatesREPL {
465
465
  lastCleanedOutput = ""; // last teammate output for clipboard copy
466
466
  dispatching = false;
467
467
  autoApproveHandoffs = false;
468
+ /** Read .teammates/settings.json (returns { version, services, ... } or defaults). */
469
+ readSettings() {
470
+ try {
471
+ const raw = JSON.parse(readFileSync(join(this.teammatesDir, "settings.json"), "utf-8"));
472
+ return {
473
+ version: raw.version ?? 1,
474
+ services: Array.isArray(raw.services) ? raw.services : [],
475
+ ...raw,
476
+ };
477
+ }
478
+ catch {
479
+ return { version: 1, services: [] };
480
+ }
481
+ }
482
+ /** Write .teammates/settings.json. */
483
+ writeSettings(settings) {
484
+ writeFileSync(join(this.teammatesDir, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`);
485
+ }
486
+ /** Check whether a specific service is installed. */
487
+ isServiceInstalled(name) {
488
+ return this.readSettings().services.some((s) => s.name === name);
489
+ }
468
490
  /** Pending handoffs awaiting user approval. */
469
491
  pendingHandoffs = [];
470
492
  /** Pending retro proposals awaiting user approval. */
@@ -1420,14 +1442,17 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1420
1442
  chalk.cyan(teammates.join(", ")));
1421
1443
  console.log(chalk.gray(` (${files.length} files copied)`));
1422
1444
  console.log();
1445
+ // Copy framework files so the agent has TEMPLATE.md etc. available
1446
+ await copyTemplateFiles(teammatesDir);
1423
1447
  // Ask if user wants the agent to adapt teammates to this codebase
1424
1448
  console.log(chalk.white(" Adapt teammates to this codebase?"));
1425
- console.log(chalk.gray(" The agent will update ownership patterns, file paths, and boundaries."));
1449
+ console.log(chalk.gray(" The agent will scan this project, evaluate which teammates are needed,"));
1450
+ console.log(chalk.gray(" adapt their files, and create any new teammates the project needs."));
1426
1451
  console.log(chalk.gray(" You can also do this later with /init."));
1427
1452
  console.log();
1428
1453
  const adapt = await this.askChoice("Adapt now? (y/n): ", ["y", "n"]);
1429
1454
  if (adapt === "y") {
1430
- await this.runAdaptationAgent(this.adapter, projectDir, teammates);
1455
+ await this.runAdaptationAgent(this.adapter, projectDir, teammates, sourceDir);
1431
1456
  }
1432
1457
  else {
1433
1458
  console.log(chalk.gray(" Skipped adaptation. Run /init to adapt later."));
@@ -1439,53 +1464,52 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1439
1464
  console.log();
1440
1465
  }
1441
1466
  /**
1442
- * Run the agent to adapt imported teammates' ownership/boundaries
1443
- * to the current codebase. Queues one task per teammate so the user
1444
- * can review and approve each adaptation individually.
1467
+ * Run the agent to adapt imported teammates to the current codebase.
1468
+ * Uses a single comprehensive session that scans the project, evaluates
1469
+ * which teammates to keep/drop/create, adapts kept teammates (with
1470
+ * Previous Projects sections), and creates any new teammates needed.
1445
1471
  */
1446
- async runAdaptationAgent(adapter, projectDir, teammateNames) {
1472
+ async runAdaptationAgent(adapter, projectDir, teammateNames, sourceProjectPath) {
1447
1473
  const teammatesDir = join(projectDir, ".teammates");
1448
1474
  console.log();
1449
- console.log(chalk.blue(" Queuing adaptation tasks...") +
1450
- chalk.gray(` ${this.adapterName} will adapt each teammate individually`));
1475
+ console.log(chalk.blue(" Starting adaptation...") +
1476
+ chalk.gray(` ${this.adapterName} will scan this project and adapt the team`));
1451
1477
  console.log();
1452
- for (const name of teammateNames) {
1453
- const prompt = await buildAdaptationPrompt(teammatesDir, name);
1454
- const tempConfig = {
1455
- name: this.adapterName,
1456
- role: "Adaptation agent",
1457
- soul: "",
1458
- wisdom: "",
1459
- dailyLogs: [],
1460
- weeklyLogs: [],
1461
- ownership: { primary: [], secondary: [] },
1462
- routingKeywords: [],
1463
- };
1464
- const sessionId = await adapter.startSession(tempConfig);
1465
- const spinner = ora({
1466
- text: chalk.blue(this.adapterName) +
1467
- chalk.gray(` is adapting @${name} to this codebase...`),
1468
- spinner: "dots",
1469
- }).start();
1470
- try {
1471
- const result = await adapter.executeTask(sessionId, tempConfig, prompt);
1472
- spinner.stop();
1473
- this.printAgentOutput(result.rawOutput);
1474
- if (result.success) {
1475
- console.log(chalk.green(` ✔ @${name} adaptation complete!`));
1476
- }
1477
- else {
1478
- console.log(chalk.yellow(` ⚠ @${name} adaptation finished with issues: ${result.summary}`));
1479
- }
1480
- }
1481
- catch (err) {
1482
- spinner.fail(chalk.red(`@${name} adaptation failed: ${err.message}`));
1478
+ const prompt = await buildImportAdaptationPrompt(teammatesDir, teammateNames, sourceProjectPath);
1479
+ const tempConfig = {
1480
+ name: this.adapterName,
1481
+ role: "Adaptation agent",
1482
+ soul: "",
1483
+ wisdom: "",
1484
+ dailyLogs: [],
1485
+ weeklyLogs: [],
1486
+ ownership: { primary: [], secondary: [] },
1487
+ routingKeywords: [],
1488
+ };
1489
+ const sessionId = await adapter.startSession(tempConfig);
1490
+ const spinner = ora({
1491
+ text: chalk.blue(this.adapterName) +
1492
+ chalk.gray(" is scanning the project and adapting teammates..."),
1493
+ spinner: "dots",
1494
+ }).start();
1495
+ try {
1496
+ const result = await adapter.executeTask(sessionId, tempConfig, prompt);
1497
+ spinner.stop();
1498
+ this.printAgentOutput(result.rawOutput);
1499
+ if (result.success) {
1500
+ console.log(chalk.green(" ✔ Team adaptation complete!"));
1483
1501
  }
1484
- if (adapter.destroySession) {
1485
- await adapter.destroySession(sessionId);
1502
+ else {
1503
+ console.log(chalk.yellow(` ⚠ Adaptation finished with issues: ${result.summary}`));
1486
1504
  }
1487
- console.log();
1488
1505
  }
1506
+ catch (err) {
1507
+ spinner.fail(chalk.red(`Adaptation failed: ${err.message}`));
1508
+ }
1509
+ if (adapter.destroySession) {
1510
+ await adapter.destroySession(sessionId);
1511
+ }
1512
+ console.log();
1489
1513
  }
1490
1514
  /**
1491
1515
  * Simple blocking prompt — reads one line from stdin and validates.
@@ -1950,6 +1974,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1950
1974
  weeklyLogs: [],
1951
1975
  ownership: { primary: [], secondary: [] },
1952
1976
  routingKeywords: [],
1977
+ cwd: dirname(this.teammatesDir),
1953
1978
  });
1954
1979
  // Add status entry (init() already ran, so we add it manually)
1955
1980
  this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle" });
@@ -1963,21 +1988,15 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1963
1988
  return { name: t.name, role: t.role, ownership: t.ownership };
1964
1989
  });
1965
1990
  }
1966
- // Detect installed services from services.json and tell the adapter
1991
+ // Detect installed services from settings.json and tell the adapter
1967
1992
  if ("services" in this.adapter) {
1968
1993
  const services = [];
1969
- try {
1970
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
1971
- if (svcJson && "recall" in svcJson) {
1972
- services.push({
1973
- name: "recall",
1974
- description: "Local semantic search across teammate memories and daily logs. Use this to find relevant context before starting a task.",
1975
- usage: 'teammates-recall search "your query" --dir .teammates',
1976
- });
1977
- }
1978
- }
1979
- catch {
1980
- /* no services.json or invalid */
1994
+ if (this.isServiceInstalled("recall")) {
1995
+ services.push({
1996
+ name: "recall",
1997
+ description: "Local semantic search across teammate memories and daily logs. Use this to find relevant context before starting a task.",
1998
+ usage: 'teammates-recall search "your query" --dir .teammates',
1999
+ });
1981
2000
  }
1982
2001
  this.adapter.services = services;
1983
2002
  }
@@ -2034,14 +2053,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2034
2053
  // ── Build animated banner for ChatView ─────────────────────────────
2035
2054
  const names = this.orchestrator.listTeammates();
2036
2055
  const reg = this.orchestrator.getRegistry();
2037
- let hasRecall = false;
2038
- try {
2039
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
2040
- hasRecall = !!(svcJson && "recall" in svcJson);
2041
- }
2042
- catch {
2043
- /* no services.json */
2044
- }
2056
+ const hasRecall = this.isServiceInstalled("recall");
2045
2057
  const bannerWidget = new AnimatedBanner({
2046
2058
  adapterName: this.adapterName,
2047
2059
  teammateCount: names.length,
@@ -2432,15 +2444,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2432
2444
  printBanner(teammates) {
2433
2445
  const registry = this.orchestrator.getRegistry();
2434
2446
  const termWidth = process.stdout.columns || 100;
2435
- // Detect recall from services.json
2436
- let recallInstalled = false;
2437
- try {
2438
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
2439
- recallInstalled = !!(svcJson && "recall" in svcJson);
2440
- }
2441
- catch {
2442
- /* no services.json or invalid */
2443
- }
2447
+ // Detect recall from settings.json
2448
+ const recallInstalled = this.isServiceInstalled("recall");
2444
2449
  this.feedLine();
2445
2450
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2446
2451
  this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
@@ -2782,45 +2787,61 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2782
2787
  }
2783
2788
  async cmdDebug(argsStr) {
2784
2789
  const arg = argsStr.trim().replace(/^@/, "");
2785
- // Resolve targets
2786
- let targets;
2790
+ // Resolve which teammates to show debug info for
2791
+ let targetNames;
2787
2792
  if (arg === "everyone") {
2788
- targets = [];
2789
- for (const [name, result] of this.lastResults) {
2790
- if (name !== this.adapterName && result.rawOutput) {
2791
- targets.push({ name, result });
2792
- }
2793
+ targetNames = [];
2794
+ for (const [name] of this.lastResults) {
2795
+ if (name !== this.adapterName)
2796
+ targetNames.push(name);
2793
2797
  }
2794
- if (targets.length === 0) {
2795
- this.feedLine(tp.muted(" No raw output available from any teammate."));
2798
+ if (targetNames.length === 0) {
2799
+ this.feedLine(tp.muted(" No debug info available from any teammate."));
2796
2800
  this.refreshView();
2797
2801
  return;
2798
2802
  }
2799
2803
  }
2804
+ else if (arg) {
2805
+ targetNames = [arg];
2806
+ }
2807
+ else if (this.lastResult) {
2808
+ targetNames = [this.lastResult.teammate];
2809
+ }
2800
2810
  else {
2801
- const result = arg ? this.lastResults.get(arg) : this.lastResult;
2802
- if (!result?.rawOutput) {
2803
- this.feedLine(tp.muted(" No raw output available." +
2804
- (arg ? "" : " Try: /debug <teammate>")));
2805
- this.refreshView();
2806
- return;
2807
- }
2808
- targets = [{ name: result.teammate, result }];
2811
+ this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate]"));
2812
+ this.refreshView();
2813
+ return;
2809
2814
  }
2810
- for (const { name, result } of targets) {
2811
- this.feedLine();
2812
- this.feedLine(tp.muted(` ── raw output from ${name} ──`));
2815
+ let debugText = "";
2816
+ for (const name of targetNames) {
2813
2817
  this.feedLine();
2814
- this.feedMarkdown(result.rawOutput);
2818
+ this.feedLine(tp.muted(` ── debug for ${name} ──`));
2819
+ // Read the last debug entry from the session file
2820
+ const sessionEntry = this.readLastDebugEntry(name);
2821
+ if (sessionEntry) {
2822
+ this.feedLine();
2823
+ this.feedMarkdown(sessionEntry);
2824
+ debugText += (debugText ? "\n\n" : "") + sessionEntry;
2825
+ }
2826
+ else {
2827
+ // Fall back to raw output from lastResults
2828
+ const result = this.lastResults.get(name);
2829
+ if (result?.rawOutput) {
2830
+ this.feedLine();
2831
+ this.feedMarkdown(result.rawOutput);
2832
+ debugText += (debugText ? "\n\n" : "") + result.rawOutput;
2833
+ }
2834
+ else {
2835
+ this.feedLine(tp.muted(" No debug info available."));
2836
+ }
2837
+ }
2815
2838
  this.feedLine();
2816
- this.feedLine(tp.muted(" ── end raw output ──"));
2839
+ this.feedLine(tp.muted(" ── end debug ──"));
2817
2840
  }
2818
2841
  // [copy] action for the debug output
2819
- if (this.chatView) {
2842
+ if (this.chatView && debugText) {
2820
2843
  const t = theme();
2821
- this.lastCleanedOutput = targets
2822
- .map((t) => t.result.rawOutput)
2823
- .join("\n\n");
2844
+ this.lastCleanedOutput = debugText;
2824
2845
  this.chatView.appendActionList([
2825
2846
  {
2826
2847
  id: "copy",
@@ -2838,6 +2859,28 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2838
2859
  this.feedLine();
2839
2860
  this.refreshView();
2840
2861
  }
2862
+ /**
2863
+ * Read the last debug entry from a teammate's session file.
2864
+ * Debug entries are delimited by "## Debug — " headings.
2865
+ */
2866
+ readLastDebugEntry(teammate) {
2867
+ try {
2868
+ const sessionFile = this.adapter.getSessionFile?.(teammate);
2869
+ if (!sessionFile)
2870
+ return null;
2871
+ const content = readFileSync(sessionFile, "utf-8");
2872
+ // Split on debug entry headings and return the last one
2873
+ const entries = content.split(/(?=^## Debug — )/m);
2874
+ const last = entries[entries.length - 1];
2875
+ if (last && last.startsWith("## Debug — ")) {
2876
+ return last.trim();
2877
+ }
2878
+ return null;
2879
+ }
2880
+ catch {
2881
+ return null;
2882
+ }
2883
+ }
2841
2884
  async cmdCancel(argsStr) {
2842
2885
  const n = parseInt(argsStr.trim(), 10);
2843
2886
  if (Number.isNaN(n) || n < 1 || n > this.taskQueue.length) {
@@ -2862,6 +2905,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2862
2905
  break;
2863
2906
  const entry = this.taskQueue.splice(idx, 1)[0];
2864
2907
  this.agentActive.set(agent, entry);
2908
+ const startTime = Date.now();
2865
2909
  try {
2866
2910
  if (entry.type === "compact") {
2867
2911
  await this.runCompact(entry.teammate);
@@ -2874,6 +2918,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2874
2918
  task: entry.task,
2875
2919
  extraContext: extraContext || undefined,
2876
2920
  });
2921
+ // Write debug entry to session file
2922
+ this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
2877
2923
  // btw results are not stored in conversation history
2878
2924
  if (entry.type !== "btw") {
2879
2925
  this.storeResult(result);
@@ -2884,6 +2930,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2884
2930
  }
2885
2931
  }
2886
2932
  catch (err) {
2933
+ // Write error debug entry to session file
2934
+ this.writeDebugEntry(entry.teammate, entry.task, null, startTime, err);
2887
2935
  // Handle spawn failures, network errors, etc. gracefully
2888
2936
  this.activeTasks.delete(agent);
2889
2937
  if (this.activeTasks.size === 0)
@@ -2895,6 +2943,52 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2895
2943
  this.agentActive.delete(agent);
2896
2944
  }
2897
2945
  }
2946
+ /**
2947
+ * Append a debug entry to the teammate's session file.
2948
+ * Captures task prompt, result summary, raw output, timing, and errors.
2949
+ */
2950
+ writeDebugEntry(teammate, task, result, startTime, error) {
2951
+ try {
2952
+ const sessionFile = this.adapter.getSessionFile?.(teammate);
2953
+ if (!sessionFile)
2954
+ return;
2955
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
2956
+ const timestamp = new Date().toISOString();
2957
+ const lines = [
2958
+ "",
2959
+ `## Debug — ${timestamp}`,
2960
+ "",
2961
+ `**Duration:** ${elapsed}s`,
2962
+ `**Task:** ${task.length > 200 ? `${task.slice(0, 200)}…` : task}`,
2963
+ "",
2964
+ ];
2965
+ if (error) {
2966
+ lines.push(`**Status:** ERROR`);
2967
+ lines.push(`**Error:** ${error?.message ?? String(error)}`);
2968
+ }
2969
+ else if (result) {
2970
+ lines.push(`**Status:** ${result.success ? "OK" : "FAILED"}`);
2971
+ lines.push(`**Summary:** ${result.summary || "(no summary)"}`);
2972
+ if (result.changedFiles.length > 0) {
2973
+ lines.push(`**Changed files:** ${result.changedFiles.join(", ")}`);
2974
+ }
2975
+ if (result.handoffs.length > 0) {
2976
+ lines.push(`**Handoffs:** ${result.handoffs.map((h) => `@${h.to}`).join(", ")}`);
2977
+ }
2978
+ lines.push("");
2979
+ lines.push("<details><summary>Raw output</summary>");
2980
+ lines.push("");
2981
+ lines.push(result.rawOutput ?? "(empty)");
2982
+ lines.push("");
2983
+ lines.push("</details>");
2984
+ }
2985
+ lines.push("");
2986
+ appendFileSync(sessionFile, lines.join("\n"), "utf-8");
2987
+ }
2988
+ catch {
2989
+ // Don't let debug logging break task execution
2990
+ }
2991
+ }
2898
2992
  async cmdInit(argsStr) {
2899
2993
  const cwd = process.cwd();
2900
2994
  const teammatesDir = join(cwd, ".teammates");
@@ -2924,16 +3018,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2924
3018
  return;
2925
3019
  }
2926
3020
  this.feedLine(tp.success(` Imported ${teammates.length} teammate${teammates.length > 1 ? "s" : ""}: ${teammates.join(", ")} (${files.length} files)`));
2927
- // Queue one adaptation task per teammate
2928
- this.feedLine(tp.muted(` Queuing ${this.adapterName} to adapt each teammate individually...`));
2929
- for (const name of teammates) {
2930
- const prompt = await buildAdaptationPrompt(teammatesDir, name);
2931
- this.taskQueue.push({
2932
- type: "agent",
2933
- teammate: this.adapterName,
2934
- task: prompt,
2935
- });
2936
- }
3021
+ // Copy framework files so the agent has TEMPLATE.md etc. available
3022
+ await copyTemplateFiles(teammatesDir);
3023
+ // Queue a single adaptation task that handles all teammates
3024
+ this.feedLine(tp.muted(` Queuing ${this.adapterName} to scan this project and adapt the team...`));
3025
+ const prompt = await buildImportAdaptationPrompt(teammatesDir, teammates, sourceDir);
3026
+ this.taskQueue.push({
3027
+ type: "agent",
3028
+ teammate: this.adapterName,
3029
+ task: prompt,
3030
+ });
2937
3031
  this.kickDrain();
2938
3032
  }
2939
3033
  catch (err) {
@@ -3022,19 +3116,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3022
3116
  return;
3023
3117
  }
3024
3118
  this.feedLine(tp.success(` ✔ ${serviceName} installed successfully`));
3025
- // Register in services.json
3026
- const svcPath = join(this.teammatesDir, "services.json");
3027
- let svcJson = {};
3028
- try {
3029
- svcJson = JSON.parse(readFileSync(svcPath, "utf-8"));
3030
- }
3031
- catch {
3032
- /* new file */
3033
- }
3034
- if (!(serviceName in svcJson)) {
3035
- svcJson[serviceName] = {};
3036
- writeFileSync(svcPath, `${JSON.stringify(svcJson, null, 2)}\n`);
3037
- this.feedLine(tp.muted(` Registered in services.json`));
3119
+ // Register in settings.json
3120
+ const settings = this.readSettings();
3121
+ if (!settings.services.some((s) => s.name === serviceName)) {
3122
+ settings.services.push({ name: serviceName });
3123
+ this.writeSettings(settings);
3124
+ this.feedLine(tp.muted(` Registered in settings.json`));
3038
3125
  }
3039
3126
  // Build initial index if this service supports it
3040
3127
  if (service.indexCmd) {
@@ -3133,15 +3220,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3133
3220
  .catch(() => { });
3134
3221
  }
3135
3222
  startRecallWatch() {
3136
- // Only start if recall is installed (check services.json)
3137
- try {
3138
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
3139
- if (!svcJson || !("recall" in svcJson))
3140
- return;
3141
- }
3142
- catch {
3143
- return; // No services.json — recall not installed
3144
- }
3223
+ // Only start if recall is installed (check settings.json)
3224
+ if (!this.isServiceInstalled("recall"))
3225
+ return;
3145
3226
  try {
3146
3227
  this.recallWatchProcess = cpSpawn("teammates-recall", ["watch", "--dir", this.teammatesDir, "--json"], {
3147
3228
  stdio: ["ignore", "ignore", "ignore"],
@@ -3245,9 +3326,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3245
3326
  if (this.chatView)
3246
3327
  this.chatView.setProgress(null);
3247
3328
  // Trigger recall sync if installed
3248
- try {
3249
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
3250
- if (svcJson && "recall" in svcJson) {
3329
+ if (this.isServiceInstalled("recall")) {
3330
+ try {
3251
3331
  if (this.chatView) {
3252
3332
  this.chatView.setProgress(`Syncing ${name} index...`);
3253
3333
  this.refreshView();
@@ -3267,9 +3347,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3267
3347
  this.feedLine(tp.success(` ✔ ${name}: index synced`));
3268
3348
  }
3269
3349
  }
3270
- }
3271
- catch {
3272
- /* recall not installed or sync failed — non-fatal */
3350
+ catch {
3351
+ /* sync failed — non-fatal */
3352
+ }
3273
3353
  }
3274
3354
  }
3275
3355
  catch (err) {
@@ -3394,14 +3474,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3394
3474
  if (teammates.length === 0)
3395
3475
  return;
3396
3476
  // Check if recall is installed
3397
- let recallInstalled = false;
3398
- try {
3399
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
3400
- recallInstalled = !!(svcJson && "recall" in svcJson);
3401
- }
3402
- catch {
3403
- /* no services.json */
3404
- }
3477
+ const recallInstalled = this.isServiceInstalled("recall");
3405
3478
  // 1. Check each teammate for stale daily logs (older than 7 days)
3406
3479
  const oneWeekAgo = new Date();
3407
3480
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
package/dist/onboard.d.ts CHANGED
@@ -28,14 +28,15 @@ export declare function importTeammates(sourceDir: string, targetDir: string): P
28
28
  files: string[];
29
29
  }>;
30
30
  /**
31
- * Build the adaptation prompt for a single imported teammate.
32
- * Tells the agent to update ownership patterns, file paths, and boundaries
33
- * for the new codebase while preserving identity, principles, and wisdom.
31
+ * Build an import-adaptation prompt that runs as a single non-interactive agent session.
32
+ * The agent scans the target project and adapts all imported teammates in one pass.
33
+ * No pauses or approval gates the agent must complete all work autonomously.
34
34
  *
35
35
  * @param teammatesDir - The .teammates/ directory in the target project
36
- * @param teammateName - The name of the teammate to adapt
36
+ * @param teammateNames - Names of all imported teammates
37
+ * @param sourceProjectPath - Path to the source project (for Previous Projects section)
37
38
  */
38
- export declare function buildAdaptationPrompt(teammatesDir: string, teammateName: string): Promise<string>;
39
+ export declare function buildImportAdaptationPrompt(teammatesDir: string, teammateNames: string[], sourceProjectPath: string): Promise<string>;
39
40
  /**
40
41
  * Load ONBOARDING.md from the project dir, package root, or built-in fallback.
41
42
  */
package/dist/onboard.js CHANGED
@@ -60,7 +60,7 @@ export async function copyTemplateFiles(teammatesDir) {
60
60
  await stat(gitignoreDest);
61
61
  }
62
62
  catch {
63
- const gitignoreContent = "USER.md\n.index/\n";
63
+ const gitignoreContent = "USER.md\n.*/\n";
64
64
  const { writeFile } = await import("node:fs/promises");
65
65
  await writeFile(gitignoreDest, gitignoreContent, "utf-8");
66
66
  copied.push(".gitignore");
@@ -92,7 +92,7 @@ export async function copyTemplateFiles(teammatesDir) {
92
92
  * Directories starting with "_" are shared non-teammate folders.
93
93
  * Files (non-directories) and special names are also excluded.
94
94
  */
95
- const NON_TEAMMATE_NAMES = new Set(["example", "services.json"]);
95
+ const NON_TEAMMATE_NAMES = new Set(["example", "settings.json"]);
96
96
  function isNonTeammateEntry(name) {
97
97
  return (name.startsWith(".") || name.startsWith("_") || NON_TEAMMATE_NAMES.has(name));
98
98
  }
@@ -182,66 +182,124 @@ export async function importTeammates(sourceDir, targetDir) {
182
182
  await stat(gitignoreDest);
183
183
  }
184
184
  catch {
185
- await writeFile(gitignoreDest, "USER.md\n.index/\n", "utf-8");
185
+ await writeFile(gitignoreDest, "USER.md\n.*/\n", "utf-8");
186
186
  files.push(".gitignore");
187
187
  }
188
188
  return { teammates, files };
189
189
  }
190
190
  /**
191
- * Build the adaptation prompt for a single imported teammate.
192
- * Tells the agent to update ownership patterns, file paths, and boundaries
193
- * for the new codebase while preserving identity, principles, and wisdom.
191
+ * Build an import-adaptation prompt that runs as a single non-interactive agent session.
192
+ * The agent scans the target project and adapts all imported teammates in one pass.
193
+ * No pauses or approval gates the agent must complete all work autonomously.
194
194
  *
195
195
  * @param teammatesDir - The .teammates/ directory in the target project
196
- * @param teammateName - The name of the teammate to adapt
196
+ * @param teammateNames - Names of all imported teammates
197
+ * @param sourceProjectPath - Path to the source project (for Previous Projects section)
197
198
  */
198
- export async function buildAdaptationPrompt(teammatesDir, teammateName) {
199
- const teammateDir = join(teammatesDir, teammateName);
200
- // Read the teammate's current SOUL.md and WISDOM.md
201
- let soulContent = "";
202
- let wisdomContent = "";
203
- try {
204
- soulContent = await readFile(join(teammateDir, "SOUL.md"), "utf-8");
205
- }
206
- catch {
207
- /* missing — agent will create from scratch */
208
- }
209
- try {
210
- wisdomContent = await readFile(join(teammateDir, "WISDOM.md"), "utf-8");
211
- }
212
- catch {
213
- /* missing — that's fine */
199
+ export async function buildImportAdaptationPrompt(teammatesDir, teammateNames, sourceProjectPath) {
200
+ const teammateSections = [];
201
+ for (const name of teammateNames) {
202
+ const dir = join(teammatesDir, name);
203
+ let soulContent = "";
204
+ let wisdomContent = "";
205
+ try {
206
+ soulContent = await readFile(join(dir, "SOUL.md"), "utf-8");
207
+ }
208
+ catch {
209
+ /* missing */
210
+ }
211
+ try {
212
+ wisdomContent = await readFile(join(dir, "WISDOM.md"), "utf-8");
213
+ }
214
+ catch {
215
+ /* missing */
216
+ }
217
+ const soulBlock = soulContent
218
+ ? `**SOUL.md:**\n\`\`\`markdown\n${soulContent}\n\`\`\``
219
+ : "*No SOUL.md found*";
220
+ const wisdomBlock = wisdomContent
221
+ ? `\n**WISDOM.md:**\n\`\`\`markdown\n${wisdomContent}\n\`\`\``
222
+ : "";
223
+ teammateSections.push(`### @${name}\n${soulBlock}${wisdomBlock}`);
214
224
  }
215
- const soulSection = soulContent
216
- ? `\n\n## Current SOUL.md\n\n\`\`\`markdown\n${soulContent}\n\`\`\``
217
- : "\n\n*No SOUL.md found — create one from the template.*";
218
- const wisdomSection = wisdomContent
219
- ? `\n\n## Current WISDOM.md\n\n\`\`\`markdown\n${wisdomContent}\n\`\`\``
220
- : "";
221
- return `You are adapting the imported teammate **${teammateName}** to this new codebase.
225
+ const projectDir = dirname(teammatesDir);
226
+ return `You are adapting an imported team to a new project. This is a non-interactive session — complete ALL work without pausing. Do not ask for confirmation or wait for user input.
227
+
228
+ **Source project:** \`${sourceProjectPath}\`
229
+ **Target project:** \`${projectDir}\`
230
+ **Target .teammates/ directory:** \`${teammatesDir}\`
231
+ **Imported teammates:** ${teammateNames.map((n) => `@${n}`).join(", ")}
232
+
233
+ > **IMPORTANT:** The \`example/\` directory inside \`.teammates/\` is a **template reference**, NOT a teammate. Do not adapt it, rename it, or treat it as a teammate. When creating new teammates, never use "example" as a folder name.
222
234
 
223
- **Teammate directory:** \`${teammateDir}\`
235
+ ## Imported Teammates (from source project)
224
236
 
225
- This teammate was imported from another project. Their SOUL.md and WISDOM.md contain identity, principles, and accumulated wisdom that should be preserved, but their **ownership patterns**, **file paths**, **boundaries**, **capabilities**, and **routing keywords** need to be updated for this codebase.
226
- ${soulSection}${wisdomSection}
237
+ ${teammateSections.join("\n\n---\n\n")}
227
238
 
228
- ## Your job:
239
+ ## Instructions
229
240
 
230
- 1. **Analyze this codebase** read the project structure, entry points, package manifest, and key files to understand the architecture.
241
+ Complete these steps in order. Do NOT pause, ask questions, or wait for approval. Make all changes directly.
231
242
 
232
- 2. **Update ${teammateName}'s SOUL.md**:
233
- - **Preserve**: Identity, Core Principles, Ethics, personality, tone
234
- - **Update**: Ownership patterns (primary/secondary file globs), Boundaries (reference correct teammate names), Capabilities (commands, file patterns, technologies), Routing keywords, Quality Bar
235
- - **Adapt**: Any codebase-specific references (paths, package names, tools)
243
+ ### Step 1: Scan This Project
236
244
 
237
- 3. **Update ${teammateName}'s WISDOM.md**:
238
- - **Preserve**: Wisdom entries that are universal (principles, patterns, lessons)
239
- - **Remove or update**: Entries referencing old project paths, file names, or architecture
240
- - **Add**: A creation entry noting this teammate was imported and adapted
245
+ Read the project root to understand its structure:
246
+ - Package manifest, README, config files
247
+ - Major subsystems, languages, frameworks, file patterns
248
+ - Dependency flow and architecture
241
249
 
242
- 4. **Verify** that ownership globs are valid for this codebase.
250
+ ### Step 2: Adapt EVERY Imported Teammate
243
251
 
244
- Present your proposed changes before applying them. Focus only on **${teammateName}** other teammates will be adapted separately.`;
252
+ This is the most important step. For EACH imported teammate listed above, you MUST edit their SOUL.md and WISDOM.md to reflect THIS project, not the source project.
253
+
254
+ For each teammate's **SOUL.md**:
255
+
256
+ 1. **Add a "Previous Projects" section** (place it after Ethics). Compress what the teammate did in the source project:
257
+ \`\`\`markdown
258
+ ## Previous Projects
259
+
260
+ ### <source-project-name>
261
+ - **Role**: <one-line summary of what they did>
262
+ - **Stack**: <key technologies they worked with>
263
+ - **Domains**: <what they owned — file patterns or subsystem names>
264
+ - **Key learnings**: <1-3 bullets of notable patterns, decisions, or lessons>
265
+ \`\`\`
266
+
267
+ 2. **Rewrite project-specific sections** for THIS project:
268
+ - **Preserve**: Identity (name, personality), Core Principles, Ethics
269
+ - **Rewrite completely**: Ownership (primary/secondary file globs for THIS project's actual files), Boundaries, Capabilities (commands, file patterns, technologies for THIS project), Routing keywords, Quality Bar
270
+ - **Update**: All codebase-specific references — paths, package names, tools, teammate names must reference THIS project
271
+
272
+ For each teammate's **WISDOM.md**:
273
+ - Add a "Previous Projects" note at the top
274
+ - Keep universal wisdom entries (general principles, patterns)
275
+ - Remove entries that reference source project paths, architecture, or tools not used here
276
+ - Adapt entries with transferable knowledge but old-project-specific details
277
+
278
+ ### Step 3: Evaluate Gaps and Create New Teammates
279
+
280
+ After adapting all existing teammates, check if THIS project has major subsystems that no teammate covers. If so, create new teammates:
281
+ - Create \`${teammatesDir}/<name>/\` with SOUL.md, WISDOM.md, and \`memory/\`
282
+ - Use the template at \`${teammatesDir}/TEMPLATE.md\` for structure
283
+ - WISDOM.md starts with one creation entry
284
+
285
+ If a teammate's domain doesn't exist at all in this project and their skills aren't transferable, delete their directory under \`${teammatesDir}\`.
286
+
287
+ ### Step 4: Update Framework Files
288
+
289
+ - Update \`${teammatesDir}/README.md\` with the final roster
290
+ - Update \`${teammatesDir}/CROSS-TEAM.md\` ownership table
291
+
292
+ ### Step 5: Verify
293
+
294
+ - Every teammate has SOUL.md and WISDOM.md adapted to THIS project
295
+ - Ownership globs reference actual files in THIS project
296
+ - Boundaries reference correct teammate names
297
+ - Previous Projects sections are present for all imported teammates
298
+ - CROSS-TEAM.md has one row per teammate
299
+
300
+ ## Critical Reminder
301
+
302
+ The PRIMARY goal is adapting the imported teammates. Every SOUL.md must be rewritten so the teammate understands THIS project's codebase, not the source project's. If you only have time for one thing, adapt the existing teammates — that is more important than creating new ones.`;
245
303
  }
246
304
  /**
247
305
  * Load ONBOARDING.md from the project dir, package root, or built-in fallback.
@@ -288,7 +346,9 @@ function wrapPrompt(onboardingContent, projectDir) {
288
346
 
289
347
  You do NOT need to create the framework files listed above — they're already there.
290
348
 
291
- Follow the onboarding instructions below. Work through each step, pausing after Step 1 and Step 2 to present your analysis and proposed roster to the user for approval before proceeding.
349
+ > **IMPORTANT:** The \`example/\` directory is a **template reference**, NOT a teammate. Do not modify it or treat it as a teammate. Never name a new teammate "example".
350
+
351
+ Follow the onboarding instructions below. This is a non-interactive session — complete ALL work without pausing. Do not ask for confirmation or wait for user input. Work through each step and make all changes directly.
292
352
 
293
353
  ---
294
354
 
@@ -312,26 +372,22 @@ Identify:
312
372
  3. **Key technologies** — languages, frameworks, tools per area
313
373
  4. **File patterns** — glob patterns for each domain
314
374
 
315
- **Present your analysis to the user and get confirmation before proceeding.**
316
-
317
375
  ## Step 2: Design the Team
318
376
 
319
- Propose a roster of teammates:
377
+ Based on your analysis, design a roster of teammates:
320
378
  - **Aim for 3–7 teammates.** Fewer for small projects, more for monorepos.
321
379
  - **Each teammate owns a distinct domain** with minimal overlap.
322
380
  - **Pick short, memorable names** — one word, evocative of the domain.
323
381
 
324
- For each proposed teammate, define:
382
+ For each teammate, define:
325
383
  - Name and one-line persona
326
384
  - Primary ownership (file patterns)
327
385
  - Key technologies
328
386
  - Boundaries (what they do NOT own)
329
387
 
330
- **Present the proposed roster to the user for approval.**
331
-
332
388
  ## Step 3: Create the Directory Structure
333
389
 
334
- Once approved, create teammate folders under \`.teammates/\`:
390
+ Create teammate folders under \`.teammates/\`:
335
391
 
336
392
  ### Teammate folders
337
393
  For each teammate, create \`.teammates/<name>/\` with:
@@ -9,6 +9,8 @@ export declare class Registry {
9
9
  private teammatesDir;
10
10
  private teammates;
11
11
  constructor(teammatesDir: string);
12
+ /** Names that are never teammates (template references, config files) */
13
+ private static NON_TEAMMATE_NAMES;
12
14
  /** Discover and load all teammates from .teammates/ */
13
15
  loadAll(): Promise<Map<string, TeammateConfig>>;
14
16
  /** Load a single teammate by name */
package/dist/registry.js CHANGED
@@ -5,17 +5,22 @@
5
5
  * (SOUL.md, WISDOM.md, daily logs, ownership rules).
6
6
  */
7
7
  import { readdir, readFile, stat } from "node:fs/promises";
8
- import { basename, join } from "node:path";
8
+ import { basename, dirname, join } from "node:path";
9
9
  export class Registry {
10
10
  teammatesDir;
11
11
  teammates = new Map();
12
12
  constructor(teammatesDir) {
13
13
  this.teammatesDir = teammatesDir;
14
14
  }
15
+ /** Names that are never teammates (template references, config files) */
16
+ static NON_TEAMMATE_NAMES = new Set(["example"]);
15
17
  /** Discover and load all teammates from .teammates/ */
16
18
  async loadAll() {
17
19
  const entries = await readdir(this.teammatesDir, { withFileTypes: true });
18
- const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_"));
20
+ const dirs = entries.filter((e) => e.isDirectory() &&
21
+ !e.name.startsWith(".") &&
22
+ !e.name.startsWith("_") &&
23
+ !Registry.NON_TEAMMATE_NAMES.has(e.name));
19
24
  for (const dir of dirs) {
20
25
  const config = await this.loadTeammate(dir.name);
21
26
  if (config) {
@@ -50,6 +55,7 @@ export class Registry {
50
55
  weeklyLogs,
51
56
  ownership,
52
57
  routingKeywords,
58
+ cwd: dirname(this.teammatesDir),
53
59
  };
54
60
  this.teammates.set(name, config);
55
61
  return config;
@@ -56,6 +56,13 @@ describe("Registry.loadAll", () => {
56
56
  await registry.loadAll();
57
57
  expect(registry.list()).toEqual(["beacon"]);
58
58
  });
59
+ it("skips the example directory even if it has SOUL.md", async () => {
60
+ await createTeammate("beacon", "# Beacon\n\nPlatform engineer.");
61
+ await createTeammate("example", "# Example\n\nTemplate reference.");
62
+ const registry = new Registry(tempDir);
63
+ await registry.loadAll();
64
+ expect(registry.list()).toEqual(["beacon"]);
65
+ });
59
66
  });
60
67
  describe("Registry.loadTeammate", () => {
61
68
  it("loads soul content", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammates/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Agent-agnostic CLI for teammates. Routes tasks, manages handoffs, and plugs into any coding agent backend.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",