@teammates/cli 0.2.2 → 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";
@@ -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.
@@ -1963,21 +1987,15 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1963
1987
  return { name: t.name, role: t.role, ownership: t.ownership };
1964
1988
  });
1965
1989
  }
1966
- // Detect installed services from services.json and tell the adapter
1990
+ // Detect installed services from settings.json and tell the adapter
1967
1991
  if ("services" in this.adapter) {
1968
1992
  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 */
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
+ });
1981
1999
  }
1982
2000
  this.adapter.services = services;
1983
2001
  }
@@ -2034,14 +2052,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2034
2052
  // ── Build animated banner for ChatView ─────────────────────────────
2035
2053
  const names = this.orchestrator.listTeammates();
2036
2054
  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
- }
2055
+ const hasRecall = this.isServiceInstalled("recall");
2045
2056
  const bannerWidget = new AnimatedBanner({
2046
2057
  adapterName: this.adapterName,
2047
2058
  teammateCount: names.length,
@@ -2432,15 +2443,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2432
2443
  printBanner(teammates) {
2433
2444
  const registry = this.orchestrator.getRegistry();
2434
2445
  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
- }
2446
+ // Detect recall from settings.json
2447
+ const recallInstalled = this.isServiceInstalled("recall");
2444
2448
  this.feedLine();
2445
2449
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2446
2450
  this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
@@ -2782,45 +2786,61 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2782
2786
  }
2783
2787
  async cmdDebug(argsStr) {
2784
2788
  const arg = argsStr.trim().replace(/^@/, "");
2785
- // Resolve targets
2786
- let targets;
2789
+ // Resolve which teammates to show debug info for
2790
+ let targetNames;
2787
2791
  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
- }
2792
+ targetNames = [];
2793
+ for (const [name] of this.lastResults) {
2794
+ if (name !== this.adapterName)
2795
+ targetNames.push(name);
2793
2796
  }
2794
- if (targets.length === 0) {
2795
- 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."));
2796
2799
  this.refreshView();
2797
2800
  return;
2798
2801
  }
2799
2802
  }
2803
+ else if (arg) {
2804
+ targetNames = [arg];
2805
+ }
2806
+ else if (this.lastResult) {
2807
+ targetNames = [this.lastResult.teammate];
2808
+ }
2800
2809
  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 }];
2810
+ this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate]"));
2811
+ this.refreshView();
2812
+ return;
2809
2813
  }
2810
- for (const { name, result } of targets) {
2811
- this.feedLine();
2812
- this.feedLine(tp.muted(` ── raw output from ${name} ──`));
2814
+ let debugText = "";
2815
+ for (const name of targetNames) {
2813
2816
  this.feedLine();
2814
- 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
+ }
2815
2837
  this.feedLine();
2816
- this.feedLine(tp.muted(" ── end raw output ──"));
2838
+ this.feedLine(tp.muted(" ── end debug ──"));
2817
2839
  }
2818
2840
  // [copy] action for the debug output
2819
- if (this.chatView) {
2841
+ if (this.chatView && debugText) {
2820
2842
  const t = theme();
2821
- this.lastCleanedOutput = targets
2822
- .map((t) => t.result.rawOutput)
2823
- .join("\n\n");
2843
+ this.lastCleanedOutput = debugText;
2824
2844
  this.chatView.appendActionList([
2825
2845
  {
2826
2846
  id: "copy",
@@ -2838,6 +2858,28 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2838
2858
  this.feedLine();
2839
2859
  this.refreshView();
2840
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
+ }
2841
2883
  async cmdCancel(argsStr) {
2842
2884
  const n = parseInt(argsStr.trim(), 10);
2843
2885
  if (Number.isNaN(n) || n < 1 || n > this.taskQueue.length) {
@@ -2862,6 +2904,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2862
2904
  break;
2863
2905
  const entry = this.taskQueue.splice(idx, 1)[0];
2864
2906
  this.agentActive.set(agent, entry);
2907
+ const startTime = Date.now();
2865
2908
  try {
2866
2909
  if (entry.type === "compact") {
2867
2910
  await this.runCompact(entry.teammate);
@@ -2874,6 +2917,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2874
2917
  task: entry.task,
2875
2918
  extraContext: extraContext || undefined,
2876
2919
  });
2920
+ // Write debug entry to session file
2921
+ this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
2877
2922
  // btw results are not stored in conversation history
2878
2923
  if (entry.type !== "btw") {
2879
2924
  this.storeResult(result);
@@ -2884,6 +2929,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2884
2929
  }
2885
2930
  }
2886
2931
  catch (err) {
2932
+ // Write error debug entry to session file
2933
+ this.writeDebugEntry(entry.teammate, entry.task, null, startTime, err);
2887
2934
  // Handle spawn failures, network errors, etc. gracefully
2888
2935
  this.activeTasks.delete(agent);
2889
2936
  if (this.activeTasks.size === 0)
@@ -2895,6 +2942,52 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2895
2942
  this.agentActive.delete(agent);
2896
2943
  }
2897
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
+ }
2898
2991
  async cmdInit(argsStr) {
2899
2992
  const cwd = process.cwd();
2900
2993
  const teammatesDir = join(cwd, ".teammates");
@@ -2924,16 +3017,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2924
3017
  return;
2925
3018
  }
2926
3019
  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
- }
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
+ });
2937
3030
  this.kickDrain();
2938
3031
  }
2939
3032
  catch (err) {
@@ -3022,19 +3115,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3022
3115
  return;
3023
3116
  }
3024
3117
  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`));
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`));
3038
3124
  }
3039
3125
  // Build initial index if this service supports it
3040
3126
  if (service.indexCmd) {
@@ -3133,15 +3219,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3133
3219
  .catch(() => { });
3134
3220
  }
3135
3221
  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
- }
3222
+ // Only start if recall is installed (check settings.json)
3223
+ if (!this.isServiceInstalled("recall"))
3224
+ return;
3145
3225
  try {
3146
3226
  this.recallWatchProcess = cpSpawn("teammates-recall", ["watch", "--dir", this.teammatesDir, "--json"], {
3147
3227
  stdio: ["ignore", "ignore", "ignore"],
@@ -3245,9 +3325,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3245
3325
  if (this.chatView)
3246
3326
  this.chatView.setProgress(null);
3247
3327
  // 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) {
3328
+ if (this.isServiceInstalled("recall")) {
3329
+ try {
3251
3330
  if (this.chatView) {
3252
3331
  this.chatView.setProgress(`Syncing ${name} index...`);
3253
3332
  this.refreshView();
@@ -3267,9 +3346,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3267
3346
  this.feedLine(tp.success(` ✔ ${name}: index synced`));
3268
3347
  }
3269
3348
  }
3270
- }
3271
- catch {
3272
- /* recall not installed or sync failed — non-fatal */
3349
+ catch {
3350
+ /* sync failed — non-fatal */
3351
+ }
3273
3352
  }
3274
3353
  }
3275
3354
  catch (err) {
@@ -3394,14 +3473,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3394
3473
  if (teammates.length === 0)
3395
3474
  return;
3396
3475
  // 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
- }
3476
+ const recallInstalled = this.isServiceInstalled("recall");
3405
3477
  // 1. Check each teammate for stale daily logs (older than 7 days)
3406
3478
  const oneWeekAgo = new Date();
3407
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.2",
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",