@teammates/cli 0.2.1 → 0.2.3

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.
@@ -269,6 +269,9 @@ export class CliProxyAdapter {
269
269
  await unlink(promptFile).catch(() => { });
270
270
  }
271
271
  }
272
+ getSessionFile(teammateName) {
273
+ return this.sessionFiles.get(teammateName);
274
+ }
272
275
  async destroySession(_sessionId) {
273
276
  // Clean up any leaked temp prompt files
274
277
  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.
@@ -173,6 +173,9 @@ export class CopilotAdapter {
173
173
  await session.disconnect().catch(() => { });
174
174
  }
175
175
  }
176
+ getSessionFile(teammateName) {
177
+ return this.sessionFiles.get(teammateName);
178
+ }
176
179
  async destroySession(_sessionId) {
177
180
  // Disconnect all sessions
178
181
  for (const [, session] of this.sessions) {
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@
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
14
  import { join, resolve } from "node:path";
@@ -19,13 +19,12 @@ import { App, ChatView, Control, concat, esc, Interview, pen, renderMarkdown, St
19
19
  import chalk from "chalk";
20
20
  import ora from "ora";
21
21
  import { CliProxyAdapter, PRESETS } from "./adapters/cli-proxy.js";
22
- import { CopilotAdapter } from "./adapters/copilot.js";
23
22
  import { EchoAdapter } from "./adapters/echo.js";
24
23
  import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
25
24
  import { compactEpisodic } from "./compact.js";
26
25
  import { PromptInput } from "./console/prompt-input.js";
27
26
  import { buildTitle } from "./console/startup.js";
28
- import { buildAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
27
+ import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
29
28
  import { Orchestrator } from "./orchestrator.js";
30
29
  import { colorToHex, theme } from "./theme.js";
31
30
  // ─── Version ─────────────────────────────────────────────────────────
@@ -87,11 +86,13 @@ async function findTeammatesDir() {
87
86
  }
88
87
  return null;
89
88
  }
90
- function resolveAdapter(name) {
89
+ async function resolveAdapter(name) {
91
90
  if (name === "echo")
92
91
  return new EchoAdapter();
93
- // GitHub Copilot SDK adapter
92
+ // GitHub Copilot SDK adapter — lazy-loaded to avoid pulling in
93
+ // @github/copilot-sdk (and vscode-jsonrpc) when not needed.
94
94
  if (name === "copilot") {
95
+ const { CopilotAdapter } = await import("./adapters/copilot.js");
95
96
  return new CopilotAdapter({
96
97
  model: modelOverride,
97
98
  });
@@ -464,6 +465,28 @@ class TeammatesREPL {
464
465
  lastCleanedOutput = ""; // last teammate output for clipboard copy
465
466
  dispatching = false;
466
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
+ }
467
490
  /** Pending handoffs awaiting user approval. */
468
491
  pendingHandoffs = [];
469
492
  /** Pending retro proposals awaiting user approval. */
@@ -1419,14 +1442,17 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1419
1442
  chalk.cyan(teammates.join(", ")));
1420
1443
  console.log(chalk.gray(` (${files.length} files copied)`));
1421
1444
  console.log();
1445
+ // Copy framework files so the agent has TEMPLATE.md etc. available
1446
+ await copyTemplateFiles(teammatesDir);
1422
1447
  // Ask if user wants the agent to adapt teammates to this codebase
1423
1448
  console.log(chalk.white(" Adapt teammates to this codebase?"));
1424
- 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."));
1425
1451
  console.log(chalk.gray(" You can also do this later with /init."));
1426
1452
  console.log();
1427
1453
  const adapt = await this.askChoice("Adapt now? (y/n): ", ["y", "n"]);
1428
1454
  if (adapt === "y") {
1429
- await this.runAdaptationAgent(this.adapter, projectDir, teammates);
1455
+ await this.runAdaptationAgent(this.adapter, projectDir, teammates, sourceDir);
1430
1456
  }
1431
1457
  else {
1432
1458
  console.log(chalk.gray(" Skipped adaptation. Run /init to adapt later."));
@@ -1438,53 +1464,52 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1438
1464
  console.log();
1439
1465
  }
1440
1466
  /**
1441
- * Run the agent to adapt imported teammates' ownership/boundaries
1442
- * to the current codebase. Queues one task per teammate so the user
1443
- * 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.
1444
1471
  */
1445
- async runAdaptationAgent(adapter, projectDir, teammateNames) {
1472
+ async runAdaptationAgent(adapter, projectDir, teammateNames, sourceProjectPath) {
1446
1473
  const teammatesDir = join(projectDir, ".teammates");
1447
1474
  console.log();
1448
- console.log(chalk.blue(" Queuing adaptation tasks...") +
1449
- 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`));
1450
1477
  console.log();
1451
- for (const name of teammateNames) {
1452
- const prompt = await buildAdaptationPrompt(teammatesDir, name);
1453
- const tempConfig = {
1454
- name: this.adapterName,
1455
- role: "Adaptation agent",
1456
- soul: "",
1457
- wisdom: "",
1458
- dailyLogs: [],
1459
- weeklyLogs: [],
1460
- ownership: { primary: [], secondary: [] },
1461
- routingKeywords: [],
1462
- };
1463
- const sessionId = await adapter.startSession(tempConfig);
1464
- const spinner = ora({
1465
- text: chalk.blue(this.adapterName) +
1466
- chalk.gray(` is adapting @${name} to this codebase...`),
1467
- spinner: "dots",
1468
- }).start();
1469
- try {
1470
- const result = await adapter.executeTask(sessionId, tempConfig, prompt);
1471
- spinner.stop();
1472
- this.printAgentOutput(result.rawOutput);
1473
- if (result.success) {
1474
- console.log(chalk.green(` ✔ @${name} adaptation complete!`));
1475
- }
1476
- else {
1477
- console.log(chalk.yellow(` ⚠ @${name} adaptation finished with issues: ${result.summary}`));
1478
- }
1479
- }
1480
- catch (err) {
1481
- 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!"));
1482
1501
  }
1483
- if (adapter.destroySession) {
1484
- await adapter.destroySession(sessionId);
1502
+ else {
1503
+ console.log(chalk.yellow(` ⚠ Adaptation finished with issues: ${result.summary}`));
1485
1504
  }
1486
- console.log();
1487
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();
1488
1513
  }
1489
1514
  /**
1490
1515
  * Simple blocking prompt — reads one line from stdin and validates.
@@ -1919,7 +1944,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1919
1944
  // ─── Lifecycle ────────────────────────────────────────────────────
1920
1945
  async start() {
1921
1946
  let teammatesDir = await findTeammatesDir();
1922
- const adapter = resolveAdapter(this.adapterName);
1947
+ const adapter = await resolveAdapter(this.adapterName);
1923
1948
  this.adapter = adapter;
1924
1949
  // No .teammates/ found — offer onboarding or solo mode
1925
1950
  if (!teammatesDir) {
@@ -1962,21 +1987,15 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1962
1987
  return { name: t.name, role: t.role, ownership: t.ownership };
1963
1988
  });
1964
1989
  }
1965
- // Detect installed services from services.json and tell the adapter
1990
+ // Detect installed services from settings.json and tell the adapter
1966
1991
  if ("services" in this.adapter) {
1967
1992
  const services = [];
1968
- try {
1969
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
1970
- if (svcJson && "recall" in svcJson) {
1971
- services.push({
1972
- name: "recall",
1973
- description: "Local semantic search across teammate memories and daily logs. Use this to find relevant context before starting a task.",
1974
- usage: 'teammates-recall search "your query" --dir .teammates',
1975
- });
1976
- }
1977
- }
1978
- catch {
1979
- /* no services.json or invalid */
1993
+ if (this.isServiceInstalled("recall")) {
1994
+ services.push({
1995
+ name: "recall",
1996
+ description: "Local semantic search across teammate memories and daily logs. Use this to find relevant context before starting a task.",
1997
+ usage: 'teammates-recall search "your query" --dir .teammates',
1998
+ });
1980
1999
  }
1981
2000
  this.adapter.services = services;
1982
2001
  }
@@ -2033,14 +2052,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2033
2052
  // ── Build animated banner for ChatView ─────────────────────────────
2034
2053
  const names = this.orchestrator.listTeammates();
2035
2054
  const reg = this.orchestrator.getRegistry();
2036
- let hasRecall = false;
2037
- try {
2038
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
2039
- hasRecall = !!(svcJson && "recall" in svcJson);
2040
- }
2041
- catch {
2042
- /* no services.json */
2043
- }
2055
+ const hasRecall = this.isServiceInstalled("recall");
2044
2056
  const bannerWidget = new AnimatedBanner({
2045
2057
  adapterName: this.adapterName,
2046
2058
  teammateCount: names.length,
@@ -2431,15 +2443,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2431
2443
  printBanner(teammates) {
2432
2444
  const registry = this.orchestrator.getRegistry();
2433
2445
  const termWidth = process.stdout.columns || 100;
2434
- // Detect recall from services.json
2435
- let recallInstalled = false;
2436
- try {
2437
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
2438
- recallInstalled = !!(svcJson && "recall" in svcJson);
2439
- }
2440
- catch {
2441
- /* no services.json or invalid */
2442
- }
2446
+ // Detect recall from settings.json
2447
+ const recallInstalled = this.isServiceInstalled("recall");
2443
2448
  this.feedLine();
2444
2449
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2445
2450
  this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
@@ -2781,45 +2786,61 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2781
2786
  }
2782
2787
  async cmdDebug(argsStr) {
2783
2788
  const arg = argsStr.trim().replace(/^@/, "");
2784
- // Resolve targets
2785
- let targets;
2789
+ // Resolve which teammates to show debug info for
2790
+ let targetNames;
2786
2791
  if (arg === "everyone") {
2787
- targets = [];
2788
- for (const [name, result] of this.lastResults) {
2789
- if (name !== this.adapterName && result.rawOutput) {
2790
- targets.push({ name, result });
2791
- }
2792
+ targetNames = [];
2793
+ for (const [name] of this.lastResults) {
2794
+ if (name !== this.adapterName)
2795
+ targetNames.push(name);
2792
2796
  }
2793
- if (targets.length === 0) {
2794
- this.feedLine(tp.muted(" No raw output available from any teammate."));
2797
+ if (targetNames.length === 0) {
2798
+ this.feedLine(tp.muted(" No debug info available from any teammate."));
2795
2799
  this.refreshView();
2796
2800
  return;
2797
2801
  }
2798
2802
  }
2803
+ else if (arg) {
2804
+ targetNames = [arg];
2805
+ }
2806
+ else if (this.lastResult) {
2807
+ targetNames = [this.lastResult.teammate];
2808
+ }
2799
2809
  else {
2800
- const result = arg ? this.lastResults.get(arg) : this.lastResult;
2801
- if (!result?.rawOutput) {
2802
- this.feedLine(tp.muted(" No raw output available." +
2803
- (arg ? "" : " Try: /debug <teammate>")));
2804
- this.refreshView();
2805
- return;
2806
- }
2807
- targets = [{ name: result.teammate, result }];
2810
+ this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate]"));
2811
+ this.refreshView();
2812
+ return;
2808
2813
  }
2809
- for (const { name, result } of targets) {
2810
- this.feedLine();
2811
- this.feedLine(tp.muted(` ── raw output from ${name} ──`));
2814
+ let debugText = "";
2815
+ for (const name of targetNames) {
2812
2816
  this.feedLine();
2813
- this.feedMarkdown(result.rawOutput);
2817
+ this.feedLine(tp.muted(` ── debug for ${name} ──`));
2818
+ // Read the last debug entry from the session file
2819
+ const sessionEntry = this.readLastDebugEntry(name);
2820
+ if (sessionEntry) {
2821
+ this.feedLine();
2822
+ this.feedMarkdown(sessionEntry);
2823
+ debugText += (debugText ? "\n\n" : "") + sessionEntry;
2824
+ }
2825
+ else {
2826
+ // Fall back to raw output from lastResults
2827
+ const result = this.lastResults.get(name);
2828
+ if (result?.rawOutput) {
2829
+ this.feedLine();
2830
+ this.feedMarkdown(result.rawOutput);
2831
+ debugText += (debugText ? "\n\n" : "") + result.rawOutput;
2832
+ }
2833
+ else {
2834
+ this.feedLine(tp.muted(" No debug info available."));
2835
+ }
2836
+ }
2814
2837
  this.feedLine();
2815
- this.feedLine(tp.muted(" ── end raw output ──"));
2838
+ this.feedLine(tp.muted(" ── end debug ──"));
2816
2839
  }
2817
2840
  // [copy] action for the debug output
2818
- if (this.chatView) {
2841
+ if (this.chatView && debugText) {
2819
2842
  const t = theme();
2820
- this.lastCleanedOutput = targets
2821
- .map((t) => t.result.rawOutput)
2822
- .join("\n\n");
2843
+ this.lastCleanedOutput = debugText;
2823
2844
  this.chatView.appendActionList([
2824
2845
  {
2825
2846
  id: "copy",
@@ -2837,6 +2858,28 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2837
2858
  this.feedLine();
2838
2859
  this.refreshView();
2839
2860
  }
2861
+ /**
2862
+ * Read the last debug entry from a teammate's session file.
2863
+ * Debug entries are delimited by "## Debug — " headings.
2864
+ */
2865
+ readLastDebugEntry(teammate) {
2866
+ try {
2867
+ const sessionFile = this.adapter.getSessionFile?.(teammate);
2868
+ if (!sessionFile)
2869
+ return null;
2870
+ const content = readFileSync(sessionFile, "utf-8");
2871
+ // Split on debug entry headings and return the last one
2872
+ const entries = content.split(/(?=^## Debug — )/m);
2873
+ const last = entries[entries.length - 1];
2874
+ if (last && last.startsWith("## Debug — ")) {
2875
+ return last.trim();
2876
+ }
2877
+ return null;
2878
+ }
2879
+ catch {
2880
+ return null;
2881
+ }
2882
+ }
2840
2883
  async cmdCancel(argsStr) {
2841
2884
  const n = parseInt(argsStr.trim(), 10);
2842
2885
  if (Number.isNaN(n) || n < 1 || n > this.taskQueue.length) {
@@ -2861,6 +2904,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2861
2904
  break;
2862
2905
  const entry = this.taskQueue.splice(idx, 1)[0];
2863
2906
  this.agentActive.set(agent, entry);
2907
+ const startTime = Date.now();
2864
2908
  try {
2865
2909
  if (entry.type === "compact") {
2866
2910
  await this.runCompact(entry.teammate);
@@ -2873,6 +2917,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2873
2917
  task: entry.task,
2874
2918
  extraContext: extraContext || undefined,
2875
2919
  });
2920
+ // Write debug entry to session file
2921
+ this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
2876
2922
  // btw results are not stored in conversation history
2877
2923
  if (entry.type !== "btw") {
2878
2924
  this.storeResult(result);
@@ -2883,6 +2929,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2883
2929
  }
2884
2930
  }
2885
2931
  catch (err) {
2932
+ // Write error debug entry to session file
2933
+ this.writeDebugEntry(entry.teammate, entry.task, null, startTime, err);
2886
2934
  // Handle spawn failures, network errors, etc. gracefully
2887
2935
  this.activeTasks.delete(agent);
2888
2936
  if (this.activeTasks.size === 0)
@@ -2894,6 +2942,52 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2894
2942
  this.agentActive.delete(agent);
2895
2943
  }
2896
2944
  }
2945
+ /**
2946
+ * Append a debug entry to the teammate's session file.
2947
+ * Captures task prompt, result summary, raw output, timing, and errors.
2948
+ */
2949
+ writeDebugEntry(teammate, task, result, startTime, error) {
2950
+ try {
2951
+ const sessionFile = this.adapter.getSessionFile?.(teammate);
2952
+ if (!sessionFile)
2953
+ return;
2954
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
2955
+ const timestamp = new Date().toISOString();
2956
+ const lines = [
2957
+ "",
2958
+ `## Debug — ${timestamp}`,
2959
+ "",
2960
+ `**Duration:** ${elapsed}s`,
2961
+ `**Task:** ${task.length > 200 ? `${task.slice(0, 200)}…` : task}`,
2962
+ "",
2963
+ ];
2964
+ if (error) {
2965
+ lines.push(`**Status:** ERROR`);
2966
+ lines.push(`**Error:** ${error?.message ?? String(error)}`);
2967
+ }
2968
+ else if (result) {
2969
+ lines.push(`**Status:** ${result.success ? "OK" : "FAILED"}`);
2970
+ lines.push(`**Summary:** ${result.summary || "(no summary)"}`);
2971
+ if (result.changedFiles.length > 0) {
2972
+ lines.push(`**Changed files:** ${result.changedFiles.join(", ")}`);
2973
+ }
2974
+ if (result.handoffs.length > 0) {
2975
+ lines.push(`**Handoffs:** ${result.handoffs.map((h) => `@${h.to}`).join(", ")}`);
2976
+ }
2977
+ lines.push("");
2978
+ lines.push("<details><summary>Raw output</summary>");
2979
+ lines.push("");
2980
+ lines.push(result.rawOutput ?? "(empty)");
2981
+ lines.push("");
2982
+ lines.push("</details>");
2983
+ }
2984
+ lines.push("");
2985
+ appendFileSync(sessionFile, lines.join("\n"), "utf-8");
2986
+ }
2987
+ catch {
2988
+ // Don't let debug logging break task execution
2989
+ }
2990
+ }
2897
2991
  async cmdInit(argsStr) {
2898
2992
  const cwd = process.cwd();
2899
2993
  const teammatesDir = join(cwd, ".teammates");
@@ -2923,16 +3017,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2923
3017
  return;
2924
3018
  }
2925
3019
  this.feedLine(tp.success(` Imported ${teammates.length} teammate${teammates.length > 1 ? "s" : ""}: ${teammates.join(", ")} (${files.length} files)`));
2926
- // Queue one adaptation task per teammate
2927
- this.feedLine(tp.muted(` Queuing ${this.adapterName} to adapt each teammate individually...`));
2928
- for (const name of teammates) {
2929
- const prompt = await buildAdaptationPrompt(teammatesDir, name);
2930
- this.taskQueue.push({
2931
- type: "agent",
2932
- teammate: this.adapterName,
2933
- task: prompt,
2934
- });
2935
- }
3020
+ // Copy framework files so the agent has TEMPLATE.md etc. available
3021
+ await copyTemplateFiles(teammatesDir);
3022
+ // Queue a single adaptation task that handles all teammates
3023
+ this.feedLine(tp.muted(` Queuing ${this.adapterName} to scan this project and adapt the team...`));
3024
+ const prompt = await buildImportAdaptationPrompt(teammatesDir, teammates, sourceDir);
3025
+ this.taskQueue.push({
3026
+ type: "agent",
3027
+ teammate: this.adapterName,
3028
+ task: prompt,
3029
+ });
2936
3030
  this.kickDrain();
2937
3031
  }
2938
3032
  catch (err) {
@@ -3021,19 +3115,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3021
3115
  return;
3022
3116
  }
3023
3117
  this.feedLine(tp.success(` ✔ ${serviceName} installed successfully`));
3024
- // Register in services.json
3025
- const svcPath = join(this.teammatesDir, "services.json");
3026
- let svcJson = {};
3027
- try {
3028
- svcJson = JSON.parse(readFileSync(svcPath, "utf-8"));
3029
- }
3030
- catch {
3031
- /* new file */
3032
- }
3033
- if (!(serviceName in svcJson)) {
3034
- svcJson[serviceName] = {};
3035
- writeFileSync(svcPath, `${JSON.stringify(svcJson, null, 2)}\n`);
3036
- this.feedLine(tp.muted(` Registered in services.json`));
3118
+ // Register in settings.json
3119
+ const settings = this.readSettings();
3120
+ if (!settings.services.some((s) => s.name === serviceName)) {
3121
+ settings.services.push({ name: serviceName });
3122
+ this.writeSettings(settings);
3123
+ this.feedLine(tp.muted(` Registered in settings.json`));
3037
3124
  }
3038
3125
  // Build initial index if this service supports it
3039
3126
  if (service.indexCmd) {
@@ -3132,15 +3219,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3132
3219
  .catch(() => { });
3133
3220
  }
3134
3221
  startRecallWatch() {
3135
- // Only start if recall is installed (check services.json)
3136
- try {
3137
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
3138
- if (!svcJson || !("recall" in svcJson))
3139
- return;
3140
- }
3141
- catch {
3142
- return; // No services.json — recall not installed
3143
- }
3222
+ // Only start if recall is installed (check settings.json)
3223
+ if (!this.isServiceInstalled("recall"))
3224
+ return;
3144
3225
  try {
3145
3226
  this.recallWatchProcess = cpSpawn("teammates-recall", ["watch", "--dir", this.teammatesDir, "--json"], {
3146
3227
  stdio: ["ignore", "ignore", "ignore"],
@@ -3244,9 +3325,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3244
3325
  if (this.chatView)
3245
3326
  this.chatView.setProgress(null);
3246
3327
  // Trigger recall sync if installed
3247
- try {
3248
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
3249
- if (svcJson && "recall" in svcJson) {
3328
+ if (this.isServiceInstalled("recall")) {
3329
+ try {
3250
3330
  if (this.chatView) {
3251
3331
  this.chatView.setProgress(`Syncing ${name} index...`);
3252
3332
  this.refreshView();
@@ -3266,9 +3346,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3266
3346
  this.feedLine(tp.success(` ✔ ${name}: index synced`));
3267
3347
  }
3268
3348
  }
3269
- }
3270
- catch {
3271
- /* recall not installed or sync failed — non-fatal */
3349
+ catch {
3350
+ /* sync failed — non-fatal */
3351
+ }
3272
3352
  }
3273
3353
  }
3274
3354
  catch (err) {
@@ -3393,14 +3473,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3393
3473
  if (teammates.length === 0)
3394
3474
  return;
3395
3475
  // Check if recall is installed
3396
- let recallInstalled = false;
3397
- try {
3398
- const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
3399
- recallInstalled = !!(svcJson && "recall" in svcJson);
3400
- }
3401
- catch {
3402
- /* no services.json */
3403
- }
3476
+ const recallInstalled = this.isServiceInstalled("recall");
3404
3477
  // 1. Check each teammate for stale daily logs (older than 7 days)
3405
3478
  const oneWeekAgo = new Date();
3406
3479
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
package/dist/onboard.d.ts CHANGED
@@ -28,14 +28,19 @@ 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 a comprehensive import-adaptation prompt that runs as a single agent session.
32
+ * The agent will:
33
+ * 1. Scan the current project
34
+ * 2. Evaluate which imported teammates are needed
35
+ * 3. Adapt kept teammates (add Previous Projects section + rewrite for new project)
36
+ * 4. Create any new teammates the project needs
37
+ * 5. Remove teammates that don't apply
34
38
  *
35
39
  * @param teammatesDir - The .teammates/ directory in the target project
36
- * @param teammateName - The name of the teammate to adapt
40
+ * @param teammateNames - Names of all imported teammates
41
+ * @param sourceProjectPath - Path to the source project (for Previous Projects section)
37
42
  */
38
- export declare function buildAdaptationPrompt(teammatesDir: string, teammateName: string): Promise<string>;
43
+ export declare function buildImportAdaptationPrompt(teammatesDir: string, teammateNames: string[], sourceProjectPath: string): Promise<string>;
39
44
  /**
40
45
  * Load ONBOARDING.md from the project dir, package root, or built-in fallback.
41
46
  */
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,146 @@ 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 a comprehensive import-adaptation prompt that runs as a single agent session.
192
+ * The agent will:
193
+ * 1. Scan the current project
194
+ * 2. Evaluate which imported teammates are needed
195
+ * 3. Adapt kept teammates (add Previous Projects section + rewrite for new project)
196
+ * 4. Create any new teammates the project needs
197
+ * 5. Remove teammates that don't apply
194
198
  *
195
199
  * @param teammatesDir - The .teammates/ directory in the target project
196
- * @param teammateName - The name of the teammate to adapt
200
+ * @param teammateNames - Names of all imported teammates
201
+ * @param sourceProjectPath - Path to the source project (for Previous Projects section)
197
202
  */
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 */
203
+ export async function buildImportAdaptationPrompt(teammatesDir, teammateNames, sourceProjectPath) {
204
+ const teammateSections = [];
205
+ for (const name of teammateNames) {
206
+ const dir = join(teammatesDir, name);
207
+ let soulContent = "";
208
+ let wisdomContent = "";
209
+ try {
210
+ soulContent = await readFile(join(dir, "SOUL.md"), "utf-8");
211
+ }
212
+ catch {
213
+ /* missing */
214
+ }
215
+ try {
216
+ wisdomContent = await readFile(join(dir, "WISDOM.md"), "utf-8");
217
+ }
218
+ catch {
219
+ /* missing */
220
+ }
221
+ const soulBlock = soulContent
222
+ ? `**SOUL.md:**\n\`\`\`markdown\n${soulContent}\n\`\`\``
223
+ : "*No SOUL.md found*";
224
+ const wisdomBlock = wisdomContent
225
+ ? `\n**WISDOM.md:**\n\`\`\`markdown\n${wisdomContent}\n\`\`\``
226
+ : "";
227
+ teammateSections.push(`### @${name}\n${soulBlock}${wisdomBlock}`);
214
228
  }
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.
229
+ const projectDir = dirname(teammatesDir);
230
+ return `You are adapting an imported team to a new project.
231
+
232
+ **Source project:** \`${sourceProjectPath}\`
233
+ **Target project:** \`${projectDir}\`
234
+ **Target .teammates/ directory:** \`${teammatesDir}\`
235
+ **Imported teammates:** ${teammateNames.map((n) => `@${n}`).join(", ")}
236
+
237
+ ## Imported Teammates (from source project)
238
+
239
+ ${teammateSections.join("\n\n---\n\n")}
222
240
 
223
- **Teammate directory:** \`${teammateDir}\`
241
+ ## Instructions
224
242
 
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}
243
+ Work through these phases in order. **Pause after Phase 1 and Phase 2** to present your analysis and get user approval before making changes.
227
244
 
228
- ## Your job:
245
+ ### Phase 1: Scan This Project
229
246
 
230
- 1. **Analyze this codebase** — read the project structure, entry points, package manifest, and key files to understand the architecture.
247
+ Analyze the current project to understand its structure:
248
+ - Read the project root: package manifest, README, config files
249
+ - Identify major subsystems, languages, frameworks, file patterns
250
+ - Understand the dependency flow and architecture
231
251
 
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)
252
+ **Present your analysis to the user and wait for confirmation.**
236
253
 
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
254
+ ### Phase 2: Evaluate Imported Teammates
241
255
 
242
- 4. **Verify** that ownership globs are valid for this codebase.
256
+ For each imported teammate, decide:
257
+ - **KEEP** — their domain or expertise is relevant to this project (even if specific details need updating)
258
+ - **DROP** — their domain doesn't exist here and their skills aren't transferable
243
259
 
244
- Present your proposed changes before applying them. Focus only on **${teammateName}** other teammates will be adapted separately.`;
260
+ Also identify **gaps** major subsystems in this project that none of the imported teammates cover. Propose new teammates for these gaps.
261
+
262
+ **Present your evaluation as a structured plan and wait for user approval:**
263
+ \`\`\`
264
+ KEEP: @name1, @name2
265
+ DROP: @name3
266
+ CREATE: @newname (role description)
267
+ \`\`\`
268
+
269
+ ### Phase 3: Adapt Kept Teammates
270
+
271
+ For each KEEP teammate, edit their SOUL.md and WISDOM.md:
272
+
273
+ 1. **Add a "Previous Projects" section** to SOUL.md (place it after Ethics, before any appendix). Compress what the teammate did in the source project into a portable summary:
274
+ \`\`\`markdown
275
+ ## Previous Projects
276
+
277
+ ### <project-name>
278
+ - **Role**: <one-line summary of what they did>
279
+ - **Stack**: <key technologies they worked with>
280
+ - **Domains**: <what they owned — file patterns or subsystem names>
281
+ - **Key learnings**: <1-3 bullets of notable patterns, decisions, or lessons>
282
+ \`\`\`
283
+
284
+ 2. **Rewrite the rest of SOUL.md** for this project:
285
+ - **Preserve**: Identity (name, personality), Core Principles, Ethics
286
+ - **Rewrite**: Ownership (primary/secondary file globs for THIS project), Boundaries, Capabilities (commands, file patterns, technologies), Routing keywords, Quality Bar
287
+ - **Update**: Any codebase-specific references (paths, package names, tools, teammate names)
288
+
289
+ 3. **Update WISDOM.md**:
290
+ - **Add** a "Previous Projects" section at the top with a compressed note:
291
+ \`\`\`markdown
292
+ ## Previous Projects
293
+
294
+ ### <project-name>
295
+ - Carried over universal wisdom entries from the source project
296
+ - Project-specific entries removed or adapted
297
+ \`\`\`
298
+ - **Keep** wisdom entries that are universal (general principles, patterns, lessons)
299
+ - **Remove** entries that reference source project paths, architecture, or tools not used here
300
+ - **Adapt** entries with transferable knowledge but old-project-specific details
301
+
302
+ ### Phase 4: Handle Dropped Teammates
303
+
304
+ For each DROP teammate, delete their directory under \`${teammatesDir}\`.
305
+
306
+ ### Phase 5: Create New Teammates
307
+
308
+ For each new teammate proposed in Phase 2 (after user approval):
309
+ - Create \`${teammatesDir}/<name>/\` with SOUL.md, WISDOM.md, and \`memory/\`
310
+ - Use the template at \`${teammatesDir}/TEMPLATE.md\` for structure
311
+ - WISDOM.md starts with one creation entry
312
+
313
+ ### Phase 6: Update Framework Files
314
+
315
+ - Update \`${teammatesDir}/README.md\` with the final roster
316
+ - Update \`${teammatesDir}/CROSS-TEAM.md\` ownership table
317
+
318
+ ### Phase 7: Verify
319
+
320
+ - Every kept/new teammate has SOUL.md and WISDOM.md
321
+ - Ownership globs cover the codebase without major gaps
322
+ - Boundaries reference the correct owning teammate
323
+ - Previous Projects sections are present for all imported teammates
324
+ - CROSS-TEAM.md has one row per teammate`;
245
325
  }
246
326
  /**
247
327
  * Load ONBOARDING.md from the project dir, package root, or built-in fallback.
package/dist/registry.js CHANGED
@@ -5,7 +5,7 @@
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();
@@ -50,6 +50,7 @@ export class Registry {
50
50
  weeklyLogs,
51
51
  ownership,
52
52
  routingKeywords,
53
+ cwd: dirname(this.teammatesDir),
53
54
  };
54
55
  this.teammates.set(name, config);
55
56
  return config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammates/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",