@teammates/cli 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,21 +7,20 @@
7
7
  * teammates --adapter codex Use a specific agent adapter
8
8
  * teammates --dir <path> Override .teammates/ location
9
9
  */
10
- import { spawn as cpSpawn, exec as execCb, execSync, } from "node:child_process";
11
- import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { exec as execCb, } from "node:child_process";
11
+ import { mkdirSync, 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 { dirname, join, resolve } from "node:path";
15
15
  import { createInterface } from "node:readline";
16
- import { promisify } from "node:util";
17
- const execAsync = promisify(execCb);
18
16
  import { App, ChatView, Control, concat, esc, Interview, pen, renderMarkdown, StyledText, stripAnsi, } from "@teammates/consolonia";
19
17
  import chalk from "chalk";
20
18
  import ora from "ora";
19
+ import { syncRecallIndex } from "./adapter.js";
21
20
  import { CliProxyAdapter, PRESETS } from "./adapters/cli-proxy.js";
22
21
  import { EchoAdapter } from "./adapters/echo.js";
23
22
  import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
24
- import { compactEpisodic } from "./compact.js";
23
+ import { buildWisdomPrompt, compactEpisodic } from "./compact.js";
25
24
  import { PromptInput } from "./console/prompt-input.js";
26
25
  import { buildTitle } from "./console/startup.js";
27
26
  import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
@@ -110,24 +109,6 @@ async function resolveAdapter(name) {
110
109
  console.error(`Available adapters: ${available}`);
111
110
  process.exit(1);
112
111
  }
113
- const SERVICE_REGISTRY = {
114
- recall: {
115
- package: "@teammates/recall",
116
- checkCmd: ["teammates-recall", "--help"],
117
- indexCmd: ["teammates-recall", "index"],
118
- description: "Local semantic search for teammate memory",
119
- wireupTask: [
120
- "The `teammates-recall` service was just installed globally.",
121
- "Wire it up so every teammate knows it's available:",
122
- "",
123
- "1. Verify `teammates-recall --help` works. If it does, great. If not, figure out the correct path to the binary (check recall/package.json bin field) and note it.",
124
- "2. Read .teammates/PROTOCOL.md and .teammates/CROSS-TEAM.md.",
125
- '3. If recall is not already documented there, add a short section explaining that `teammates-recall` is now available for semantic memory search, with basic usage (e.g. `teammates-recall search "query"`).',
126
- "4. Check each teammate's SOUL.md (under .teammates/*/SOUL.md). If a teammate's role involves memory or search, note in their SOUL.md that recall is installed and available.",
127
- "5. Do NOT modify code files — only update .teammates/ markdown files.",
128
- ].join("\n"),
129
- },
130
- };
131
112
  // WordwheelItem is now DropdownItem from @teammates/consolonia
132
113
  // ── Themed pen shortcuts ────────────────────────────────────────────
133
114
  //
@@ -215,10 +196,8 @@ class AnimatedBanner extends Control {
215
196
  lines.push(concat(tp.accent(tmTop), tp.text(gap + info.adapterName), tp.muted(` · ${info.teammateCount} teammate${info.teammateCount === 1 ? "" : "s"}`), tp.muted(` · v${PKG_VERSION}`)));
216
197
  // TM logo row 2 + cwd
217
198
  lines.push(concat(tp.accent(tmBot), tp.muted(gap + info.cwd)));
218
- // Recall status (indented to align with info above)
219
- lines.push(info.recallInstalled
220
- ? concat(tp.text(tmPad + gap), tp.success("● "), tp.success("recall"), tp.muted(" installed"))
221
- : concat(tp.text(tmPad + gap), tp.warning("○ "), tp.warning("recall"), tp.muted(" not installed")));
199
+ // Recall status (bundled as dependency)
200
+ lines.push(concat(tp.text(tmPad + gap), tp.success("● "), tp.success("recall"), tp.muted(" bundled")));
222
201
  // blank
223
202
  lines.push("");
224
203
  this._rosterStart = lines.length;
@@ -241,10 +220,7 @@ class AnimatedBanner extends Control {
241
220
  ["/retro", "run retrospective"],
242
221
  ];
243
222
  const col3 = [
244
- [
245
- info.recallInstalled ? "/copy" : "/install",
246
- info.recallInstalled ? "copy session text" : "add a service",
247
- ],
223
+ ["/copy", "copy session text"],
248
224
  ["/help", "all commands"],
249
225
  ["/exit", "exit session"],
250
226
  ];
@@ -447,7 +423,6 @@ class TeammatesREPL {
447
423
  }
448
424
  adapterName;
449
425
  teammatesDir;
450
- recallWatchProcess = null;
451
426
  taskQueue = [];
452
427
  /** Per-agent active tasks — one per agent running in parallel. */
453
428
  agentActive = new Map();
@@ -465,28 +440,10 @@ class TeammatesREPL {
465
440
  lastCleanedOutput = ""; // last teammate output for clipboard copy
466
441
  dispatching = false;
467
442
  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
- }
443
+ /** Last debug log file path per teammate for /debug analysis. */
444
+ lastDebugFiles = new Map();
445
+ /** Last task prompt per teammate — for /debug analysis. */
446
+ lastTaskPrompts = new Map();
490
447
  /** Pending handoffs awaiting user approval. */
491
448
  pendingHandoffs = [];
492
449
  /** Pending retro proposals awaiting user approval. */
@@ -1239,11 +1196,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1239
1196
  this.kickDrain();
1240
1197
  return;
1241
1198
  }
1242
- // No mentions — auto-route: resolve teammate synchronously if possible, else use default
1243
- let match = this.orchestrator.route(input);
1199
+ // No mentions — default to the teammate you're chatting with, then try auto-route
1200
+ let match = null;
1201
+ if (this.lastResult) {
1202
+ match = this.lastResult.teammate;
1203
+ }
1244
1204
  if (!match) {
1245
- // Fall back to adapter name — avoid blocking for agent routing
1246
- match = this.adapterName;
1205
+ match = this.orchestrator.route(input) ?? this.adapterName;
1247
1206
  }
1248
1207
  {
1249
1208
  const bg = this._userBg;
@@ -1717,17 +1676,6 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1717
1676
  };
1718
1677
  /** Build param-completion items for the current line, if any. */
1719
1678
  getParamItems(cmdName, argsBefore, partial) {
1720
- // Service-name completions for /install
1721
- if (cmdName === "install" && !argsBefore.trim()) {
1722
- const lower = partial.toLowerCase();
1723
- return Object.entries(SERVICE_REGISTRY)
1724
- .filter(([name]) => name.startsWith(lower))
1725
- .map(([name, svc]) => ({
1726
- label: name,
1727
- description: svc.description,
1728
- completion: `/install ${name} `,
1729
- }));
1730
- }
1731
1679
  const positions = TeammatesREPL.TEAMMATE_ARG_POSITIONS[cmdName];
1732
1680
  if (!positions)
1733
1681
  return [];
@@ -1996,20 +1944,6 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1996
1944
  return { name: t.name, role: t.role, ownership: t.ownership };
1997
1945
  });
1998
1946
  }
1999
- // Detect installed services from settings.json and tell the adapter
2000
- if ("services" in this.adapter) {
2001
- const services = [];
2002
- if (this.isServiceInstalled("recall")) {
2003
- services.push({
2004
- name: "recall",
2005
- description: "Local semantic search across teammate memories and daily logs. Use this to find relevant context before starting a task.",
2006
- usage: 'teammates-recall search "your query" --dir .teammates',
2007
- });
2008
- }
2009
- this.adapter.services = services;
2010
- }
2011
- // Start recall watch mode if recall is installed
2012
- this.startRecallWatch();
2013
1947
  // Background maintenance: compact stale dailies + sync recall indexes
2014
1948
  this.startupMaintenance().catch(() => { });
2015
1949
  // Register commands
@@ -2024,6 +1958,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2024
1958
  const validNames = new Set([
2025
1959
  ...this.orchestrator.listTeammates(),
2026
1960
  this.adapterName,
1961
+ "everyone",
2027
1962
  ]);
2028
1963
  return value
2029
1964
  .replace(/@(\w+)/g, (match, name) => validNames.has(name) ? chalk.blue(match) : match)
@@ -2061,12 +1996,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2061
1996
  // ── Build animated banner for ChatView ─────────────────────────────
2062
1997
  const names = this.orchestrator.listTeammates();
2063
1998
  const reg = this.orchestrator.getRegistry();
2064
- const hasRecall = this.isServiceInstalled("recall");
2065
1999
  const bannerWidget = new AnimatedBanner({
2066
2000
  adapterName: this.adapterName,
2067
2001
  teammateCount: names.length,
2068
2002
  cwd: process.cwd(),
2069
- recallInstalled: hasRecall,
2070
2003
  teammates: names.map((name) => {
2071
2004
  const t = reg.get(name);
2072
2005
  return { name, role: t?.role ?? "" };
@@ -2098,6 +2031,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2098
2031
  const validNames = new Set([
2099
2032
  ...this.orchestrator.listTeammates(),
2100
2033
  this.adapterName,
2034
+ "everyone",
2101
2035
  ]);
2102
2036
  const mentionPattern = /@(\w+)/g;
2103
2037
  while ((m = mentionPattern.exec(value)) !== null) {
@@ -2233,7 +2167,6 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2233
2167
  this.ctrlcTimer = null;
2234
2168
  }
2235
2169
  this.chatView.setFooter(this.defaultFooter);
2236
- this.stopRecallWatch();
2237
2170
  if (this.app)
2238
2171
  this.app.stop();
2239
2172
  this.orchestrator.shutdown().then(() => process.exit(0));
@@ -2287,6 +2220,15 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2287
2220
  : `xdg-open ${quoted}`;
2288
2221
  execCb(cmd, () => { });
2289
2222
  });
2223
+ this.chatView.on("file", (filePath) => {
2224
+ const quoted = JSON.stringify(filePath);
2225
+ const cmd = process.platform === "darwin"
2226
+ ? `open ${quoted}`
2227
+ : process.platform === "win32"
2228
+ ? `start "" ${quoted}`
2229
+ : `xdg-open ${quoted}`;
2230
+ execCb(cmd, () => { });
2231
+ });
2290
2232
  this.app = new App({
2291
2233
  root: this.chatView,
2292
2234
  alternateScreen: true,
@@ -2456,15 +2398,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2456
2398
  printBanner(teammates) {
2457
2399
  const registry = this.orchestrator.getRegistry();
2458
2400
  const termWidth = process.stdout.columns || 100;
2459
- // Detect recall from settings.json
2460
- const recallInstalled = this.isServiceInstalled("recall");
2461
2401
  this.feedLine();
2462
2402
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2463
2403
  this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2464
2404
  this.feedLine(` ${process.cwd()}`);
2465
- this.feedLine(recallInstalled
2466
- ? tp.success(" ● recall installed")
2467
- : tp.warning(" ○ recall not installed"));
2405
+ this.feedLine(concat(tp.success(" ● recall"), tp.muted(" bundled")));
2468
2406
  // Roster
2469
2407
  this.feedLine();
2470
2408
  for (const name of teammates) {
@@ -2483,11 +2421,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2483
2421
  // First run — no teammates yet
2484
2422
  col1 = [
2485
2423
  ["/init", "set up teammates"],
2486
- ["/install", "add a service"],
2424
+ ["/help", "all commands"],
2487
2425
  ];
2488
2426
  col2 = [
2489
- ["/help", "all commands"],
2490
2427
  ["/exit", "exit session"],
2428
+ ["", ""],
2491
2429
  ];
2492
2430
  col3 = [
2493
2431
  ["", ""],
@@ -2506,10 +2444,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2506
2444
  ["/retro", "run retrospective"],
2507
2445
  ];
2508
2446
  col3 = [
2509
- [
2510
- recallInstalled ? "/copy" : "/install",
2511
- recallInstalled ? "copy session text" : "add a service",
2512
- ],
2447
+ ["/copy", "copy session text"],
2513
2448
  ["/help", "all commands"],
2514
2449
  ["/exit", "exit session"],
2515
2450
  ];
@@ -2540,7 +2475,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2540
2475
  name: "debug",
2541
2476
  aliases: ["raw"],
2542
2477
  usage: "/debug [teammate]",
2543
- description: "Show raw agent output from the last task",
2478
+ description: "Analyze the last agent task with the coding agent",
2544
2479
  run: (args) => this.cmdDebug(args),
2545
2480
  },
2546
2481
  {
@@ -2564,13 +2499,6 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2564
2499
  description: "Clear history and reset the session",
2565
2500
  run: () => this.cmdClear(),
2566
2501
  },
2567
- {
2568
- name: "install",
2569
- aliases: [],
2570
- usage: "/install [service]",
2571
- description: "Install a teammates service (e.g. recall)",
2572
- run: (args) => this.cmdInstall(args),
2573
- },
2574
2502
  {
2575
2503
  name: "compact",
2576
2504
  aliases: [],
@@ -2620,7 +2548,6 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2620
2548
  description: "Exit the session",
2621
2549
  run: async () => {
2622
2550
  this.feedLine(tp.muted("Shutting down..."));
2623
- this.stopRecallWatch();
2624
2551
  if (this.app)
2625
2552
  this.app.stop();
2626
2553
  await this.orchestrator.shutdown();
@@ -2812,99 +2739,81 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2812
2739
  }
2813
2740
  async cmdDebug(argsStr) {
2814
2741
  const arg = argsStr.trim().replace(/^@/, "");
2815
- // Resolve which teammates to show debug info for
2816
- let targetNames;
2742
+ // Resolve which teammate to debug
2743
+ let targetName;
2817
2744
  if (arg === "everyone") {
2818
- targetNames = [];
2819
- for (const [name] of this.lastResults) {
2745
+ // Pick all teammates with debug files, queue one analysis per teammate
2746
+ const names = [];
2747
+ for (const [name] of this.lastDebugFiles) {
2820
2748
  if (name !== this.adapterName)
2821
- targetNames.push(name);
2749
+ names.push(name);
2822
2750
  }
2823
- if (targetNames.length === 0) {
2751
+ if (names.length === 0) {
2824
2752
  this.feedLine(tp.muted(" No debug info available from any teammate."));
2825
2753
  this.refreshView();
2826
2754
  return;
2827
2755
  }
2756
+ for (const name of names) {
2757
+ this.queueDebugAnalysis(name);
2758
+ }
2759
+ return;
2828
2760
  }
2829
2761
  else if (arg) {
2830
- targetNames = [arg];
2762
+ targetName = arg;
2831
2763
  }
2832
2764
  else if (this.lastResult) {
2833
- targetNames = [this.lastResult.teammate];
2765
+ targetName = this.lastResult.teammate;
2834
2766
  }
2835
2767
  else {
2836
2768
  this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate]"));
2837
2769
  this.refreshView();
2838
2770
  return;
2839
2771
  }
2840
- let debugText = "";
2841
- for (const name of targetNames) {
2842
- this.feedLine();
2843
- this.feedLine(tp.muted(` ── debug for ${name} ──`));
2844
- // Read the last debug entry from the session file
2845
- const sessionEntry = this.readLastDebugEntry(name);
2846
- if (sessionEntry) {
2847
- this.feedLine();
2848
- this.feedMarkdown(sessionEntry);
2849
- debugText += (debugText ? "\n\n" : "") + sessionEntry;
2850
- }
2851
- else {
2852
- // Fall back to raw output from lastResults
2853
- const result = this.lastResults.get(name);
2854
- if (result?.rawOutput) {
2855
- this.feedLine();
2856
- this.feedMarkdown(result.rawOutput);
2857
- debugText += (debugText ? "\n\n" : "") + result.rawOutput;
2858
- }
2859
- else {
2860
- this.feedLine(tp.muted(" No debug info available."));
2861
- }
2862
- }
2863
- this.feedLine();
2864
- this.feedLine(tp.muted(" ── end debug ──"));
2865
- }
2866
- // [copy] action for the debug output
2867
- if (this.chatView && debugText) {
2868
- const t = theme();
2869
- this.lastCleanedOutput = debugText;
2870
- this.chatView.appendActionList([
2871
- {
2872
- id: "copy",
2873
- normalStyle: this.makeSpan({
2874
- text: " [copy]",
2875
- style: { fg: t.textDim },
2876
- }),
2877
- hoverStyle: this.makeSpan({
2878
- text: " [copy]",
2879
- style: { fg: t.accent },
2880
- }),
2881
- },
2882
- ]);
2883
- }
2884
- this.feedLine();
2885
- this.refreshView();
2772
+ this.queueDebugAnalysis(targetName);
2886
2773
  }
2887
2774
  /**
2888
- * Read the last debug entry from a teammate's session file.
2889
- * Debug entries are delimited by "## Debug — " headings.
2775
+ * Queue a debug analysis task sends the last request + debug log
2776
+ * to the base coding agent for analysis.
2890
2777
  */
2891
- readLastDebugEntry(teammate) {
2778
+ queueDebugAnalysis(teammate) {
2779
+ const debugFile = this.lastDebugFiles.get(teammate);
2780
+ const lastPrompt = this.lastTaskPrompts.get(teammate);
2781
+ if (!debugFile) {
2782
+ this.feedLine(tp.muted(` No debug log available for @${teammate}.`));
2783
+ this.refreshView();
2784
+ return;
2785
+ }
2786
+ // Read the debug log file
2787
+ let debugContent;
2892
2788
  try {
2893
- const sessionFile = this.adapter.getSessionFile?.(teammate);
2894
- if (!sessionFile)
2895
- return null;
2896
- const content = readFileSync(sessionFile, "utf-8");
2897
- // Split on debug entry headings and return the last one
2898
- const entries = content.split(/(?=^## Debug — )/m);
2899
- const last = entries[entries.length - 1];
2900
- if (last && last.startsWith("## Debug — ")) {
2901
- return last.trim();
2902
- }
2903
- return null;
2789
+ debugContent = readFileSync(debugFile, "utf-8");
2904
2790
  }
2905
2791
  catch {
2906
- return null;
2792
+ this.feedLine(tp.muted(` Could not read debug log: ${debugFile}`));
2793
+ this.refreshView();
2794
+ return;
2907
2795
  }
2796
+ const analysisPrompt = [
2797
+ `Analyze the following debug log from @${teammate}'s last task execution. Identify any issues, errors, or anomalies. If the response was empty, explain likely causes. Provide a concise diagnosis and suggest fixes if applicable.`,
2798
+ "",
2799
+ "## Last Request Sent to Agent",
2800
+ "",
2801
+ lastPrompt ?? "(not available)",
2802
+ "",
2803
+ "## Debug Log",
2804
+ "",
2805
+ debugContent,
2806
+ ].join("\n");
2807
+ // Show the debug log path — ctrl+click to open
2808
+ this.feedLine(concat(tp.muted(" Debug log: "), tp.accent(debugFile)));
2809
+ this.feedLine(tp.muted(" Queuing analysis…"));
2810
+ this.refreshView();
2811
+ this.taskQueue.push({
2812
+ type: "debug",
2813
+ teammate: this.adapterName,
2814
+ task: analysisPrompt,
2815
+ });
2816
+ this.kickDrain();
2908
2817
  }
2909
2818
  async cmdCancel(argsStr) {
2910
2819
  const n = parseInt(argsStr.trim(), 10);
@@ -2936,17 +2845,21 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2936
2845
  await this.runCompact(entry.teammate);
2937
2846
  }
2938
2847
  else {
2939
- // btw tasks skip conversation context (side question, not part of main thread)
2940
- const extraContext = entry.type === "btw" ? "" : this.buildConversationContext();
2848
+ // btw and debug tasks skip conversation context (not part of main thread)
2849
+ const extraContext = entry.type === "btw" || entry.type === "debug"
2850
+ ? ""
2851
+ : this.buildConversationContext();
2941
2852
  const result = await this.orchestrator.assign({
2942
2853
  teammate: entry.teammate,
2943
2854
  task: entry.task,
2944
2855
  extraContext: extraContext || undefined,
2945
2856
  });
2946
- // Write debug entry to session file
2947
- this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
2948
- // btw results are not stored in conversation history
2949
- if (entry.type !== "btw") {
2857
+ // Write debug entry skip for debug analysis tasks (avoid recursion)
2858
+ if (entry.type !== "debug") {
2859
+ this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
2860
+ }
2861
+ // btw and debug results are not stored in conversation history
2862
+ if (entry.type !== "btw" && entry.type !== "debug") {
2950
2863
  this.storeResult(result);
2951
2864
  }
2952
2865
  if (entry.type === "retro") {
@@ -2969,29 +2882,42 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2969
2882
  }
2970
2883
  }
2971
2884
  /**
2972
- * Append a debug entry to the teammate's session file.
2973
- * Captures task prompt, result summary, raw output, timing, and errors.
2885
+ * Write a debug log file to .teammates/.tmp/debug/ for the task.
2886
+ * Each task gets its own file. The path is stored in lastDebugFiles for /debug.
2974
2887
  */
2975
2888
  writeDebugEntry(teammate, task, result, startTime, error) {
2976
2889
  try {
2977
- const sessionFile = this.adapter.getSessionFile?.(teammate);
2978
- if (!sessionFile)
2890
+ const debugDir = join(this.teammatesDir, ".tmp", "debug");
2891
+ try {
2892
+ mkdirSync(debugDir, { recursive: true });
2893
+ }
2894
+ catch {
2979
2895
  return;
2896
+ }
2980
2897
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
2981
2898
  const timestamp = new Date().toISOString();
2899
+ const ts = timestamp.replace(/[:.]/g, "-");
2900
+ const debugFile = join(debugDir, `${teammate}-${ts}.md`);
2982
2901
  const lines = [
2902
+ `# Debug — ${teammate}`,
2983
2903
  "",
2984
- `## Debug — ${timestamp}`,
2985
- "",
2904
+ `**Timestamp:** ${timestamp}`,
2986
2905
  `**Duration:** ${elapsed}s`,
2987
- `**Task:** ${task.length > 200 ? `${task.slice(0, 200)}…` : task}`,
2906
+ "",
2907
+ "## Request",
2908
+ "",
2909
+ task,
2988
2910
  "",
2989
2911
  ];
2990
2912
  if (error) {
2913
+ lines.push("## Result");
2914
+ lines.push("");
2991
2915
  lines.push(`**Status:** ERROR`);
2992
2916
  lines.push(`**Error:** ${error?.message ?? String(error)}`);
2993
2917
  }
2994
2918
  else if (result) {
2919
+ lines.push("## Result");
2920
+ lines.push("");
2995
2921
  lines.push(`**Status:** ${result.success ? "OK" : "FAILED"}`);
2996
2922
  lines.push(`**Summary:** ${result.summary || "(no summary)"}`);
2997
2923
  if (result.changedFiles.length > 0) {
@@ -3010,30 +2936,36 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3010
2936
  lines.push(`**Signal:** ${diag.signal}`);
3011
2937
  if (diag.timedOut)
3012
2938
  lines.push(`**Timed out:** yes`);
3013
- if (diag.debugFile)
3014
- lines.push(`**Debug log:** ${diag.debugFile}`);
3015
- // Show stderr separately this is where tool call output and errors go
2939
+ if (diag.debugFile) {
2940
+ lines.push(`**Agent debug log:** ${diag.debugFile}`);
2941
+ // Inline Claude's debug file content if it exists
2942
+ try {
2943
+ const agentDebugContent = readFileSync(diag.debugFile, "utf-8");
2944
+ lines.push("");
2945
+ lines.push("### Agent Debug Log");
2946
+ lines.push("");
2947
+ lines.push(agentDebugContent);
2948
+ }
2949
+ catch {
2950
+ /* debug file may not exist yet or be unreadable */
2951
+ }
2952
+ }
3016
2953
  if (diag.stderr.trim()) {
3017
2954
  lines.push("");
3018
- lines.push("<details><summary>stderr</summary>");
2955
+ lines.push("### stderr");
3019
2956
  lines.push("");
3020
2957
  lines.push(diag.stderr);
3021
- lines.push("");
3022
- lines.push("</details>");
3023
- }
3024
- else {
3025
- lines.push(`**stderr:** (empty)`);
3026
2958
  }
3027
2959
  }
3028
2960
  lines.push("");
3029
- lines.push("<details><summary>Raw output</summary>");
2961
+ lines.push("### Raw Output");
3030
2962
  lines.push("");
3031
2963
  lines.push(result.rawOutput ?? "(empty)");
3032
- lines.push("");
3033
- lines.push("</details>");
3034
2964
  }
3035
2965
  lines.push("");
3036
- appendFileSync(sessionFile, lines.join("\n"), "utf-8");
2966
+ writeFileSync(debugFile, lines.join("\n"), "utf-8");
2967
+ this.lastDebugFiles.set(teammate, debugFile);
2968
+ this.lastTaskPrompts.set(teammate, task);
3037
2969
  }
3038
2970
  catch {
3039
2971
  // Don't let debug logging break task execution
@@ -3111,123 +3043,6 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3111
3043
  this.feedLine(tp.muted(" Run /status to see the roster."));
3112
3044
  this.refreshView();
3113
3045
  }
3114
- async cmdInstall(argsStr) {
3115
- const serviceName = argsStr.trim().toLowerCase();
3116
- if (!serviceName) {
3117
- this.feedLine(tp.bold("\n Available services:"));
3118
- for (const [name, svc] of Object.entries(SERVICE_REGISTRY)) {
3119
- this.feedLine(concat(tp.accent(name.padEnd(16)), tp.muted(svc.description)));
3120
- }
3121
- this.feedLine();
3122
- this.refreshView();
3123
- return;
3124
- }
3125
- const service = SERVICE_REGISTRY[serviceName];
3126
- if (!service) {
3127
- this.feedLine(tp.warning(` Unknown service: ${serviceName}`));
3128
- this.feedLine(tp.muted(` Available: ${Object.keys(SERVICE_REGISTRY).join(", ")}`));
3129
- this.refreshView();
3130
- return;
3131
- }
3132
- // Install the package globally
3133
- if (this.chatView) {
3134
- this.chatView.setProgress(`Installing ${service.package}...`);
3135
- this.refreshView();
3136
- }
3137
- let installSpinner = null;
3138
- if (!this.chatView) {
3139
- installSpinner = ora({
3140
- text: chalk.blue(serviceName) +
3141
- chalk.gray(` installing ${service.package}...`),
3142
- spinner: "dots",
3143
- }).start();
3144
- }
3145
- try {
3146
- await execAsync(`npm install -g ${service.package}`, {
3147
- timeout: 5 * 60 * 1000,
3148
- });
3149
- if (installSpinner)
3150
- installSpinner.stop();
3151
- if (this.chatView)
3152
- this.chatView.setProgress(null);
3153
- }
3154
- catch (err) {
3155
- if (installSpinner)
3156
- installSpinner.fail(chalk.red(`Install failed: ${err.message}`));
3157
- if (this.chatView) {
3158
- this.chatView.setProgress(null);
3159
- this.feedLine(tp.error(` ✖ Install failed: ${err.message}`));
3160
- this.refreshView();
3161
- }
3162
- return;
3163
- }
3164
- // Verify the binary works
3165
- const checkCmdStr = service.checkCmd.join(" ");
3166
- try {
3167
- execSync(checkCmdStr, { stdio: "ignore" });
3168
- }
3169
- catch {
3170
- this.feedLine(tp.success(` ✔ ${serviceName} installed`));
3171
- this.feedLine(tp.warning(` ⚠ Restart your terminal to add ${service.checkCmd[0]} to your PATH, then run /install ${serviceName} again to build the index.`));
3172
- this.refreshView();
3173
- return;
3174
- }
3175
- this.feedLine(tp.success(` ✔ ${serviceName} installed successfully`));
3176
- // Register in settings.json
3177
- const settings = this.readSettings();
3178
- if (!settings.services.some((s) => s.name === serviceName)) {
3179
- settings.services.push({ name: serviceName });
3180
- this.writeSettings(settings);
3181
- this.feedLine(tp.muted(` Registered in settings.json`));
3182
- }
3183
- // Build initial index if this service supports it
3184
- if (service.indexCmd) {
3185
- if (this.chatView) {
3186
- this.chatView.setProgress(`Building ${serviceName} index...`);
3187
- this.refreshView();
3188
- }
3189
- let idxSpinner = null;
3190
- if (!this.chatView) {
3191
- idxSpinner = ora({
3192
- text: chalk.blue(serviceName) + chalk.gray(` building index...`),
3193
- spinner: "dots",
3194
- }).start();
3195
- }
3196
- const indexCmdStr = service.indexCmd.join(" ");
3197
- try {
3198
- await execAsync(indexCmdStr, {
3199
- cwd: resolve(this.teammatesDir, ".."),
3200
- timeout: 5 * 60 * 1000,
3201
- });
3202
- if (idxSpinner)
3203
- idxSpinner.succeed(chalk.blue(serviceName) + chalk.gray(" index built"));
3204
- if (this.chatView) {
3205
- this.chatView.setProgress(null);
3206
- this.feedLine(tp.success(` ✔ ${serviceName} index built`));
3207
- }
3208
- }
3209
- catch (err) {
3210
- if (idxSpinner)
3211
- idxSpinner.warn(chalk.yellow(`Index build failed: ${err.message}`));
3212
- if (this.chatView) {
3213
- this.chatView.setProgress(null);
3214
- this.feedLine(tp.warning(` ⚠ Index build failed: ${err.message}`));
3215
- }
3216
- }
3217
- }
3218
- // Ask the coding agent to wire the service into the project
3219
- if (service.wireupTask) {
3220
- this.feedLine();
3221
- this.feedLine(tp.muted(` Wiring up ${serviceName}...`));
3222
- this.refreshView();
3223
- const result = await this.orchestrator.assign({
3224
- teammate: this.adapterName,
3225
- task: service.wireupTask,
3226
- });
3227
- this.storeResult(result);
3228
- }
3229
- this.refreshView();
3230
- }
3231
3046
  async cmdClear() {
3232
3047
  this.conversationHistory.length = 0;
3233
3048
  this.lastResult = null;
@@ -3276,35 +3091,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3276
3091
  })
3277
3092
  .catch(() => { });
3278
3093
  }
3279
- startRecallWatch() {
3280
- // Only start if recall is installed (check settings.json)
3281
- if (!this.isServiceInstalled("recall"))
3282
- return;
3283
- try {
3284
- this.recallWatchProcess = cpSpawn("teammates-recall", ["watch", "--dir", this.teammatesDir, "--json"], {
3285
- stdio: ["ignore", "ignore", "ignore"],
3286
- detached: false,
3287
- });
3288
- this.recallWatchProcess.on("error", () => {
3289
- // Recall binary not found — silently ignore
3290
- this.recallWatchProcess = null;
3291
- });
3292
- this.recallWatchProcess.on("exit", () => {
3293
- this.recallWatchProcess = null;
3294
- });
3295
- }
3296
- catch {
3297
- this.recallWatchProcess = null;
3298
- }
3299
- }
3300
- stopRecallWatch() {
3301
- if (this.recallWatchProcess) {
3302
- this.recallWatchProcess.kill("SIGTERM");
3303
- this.recallWatchProcess = null;
3304
- }
3305
- }
3094
+ // Recall is now bundled as a library dependency — no watch process needed.
3095
+ // Sync happens via syncRecallIndex() after every task and on startup.
3306
3096
  async cmdCompact(argsStr) {
3307
- const arg = argsStr.trim();
3097
+ const arg = argsStr.trim().replace(/^@/, "");
3308
3098
  const allTeammates = this.orchestrator
3309
3099
  .listTeammates()
3310
3100
  .filter((n) => n !== this.adapterName);
@@ -3382,31 +3172,46 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3382
3172
  }
3383
3173
  if (this.chatView)
3384
3174
  this.chatView.setProgress(null);
3385
- // Trigger recall sync if installed
3386
- if (this.isServiceInstalled("recall")) {
3387
- try {
3388
- if (this.chatView) {
3389
- this.chatView.setProgress(`Syncing ${name} index...`);
3390
- this.refreshView();
3391
- }
3392
- let syncSpinner = null;
3393
- if (!this.chatView) {
3394
- syncSpinner = ora({
3395
- text: `Syncing ${name} index...`,
3396
- color: "cyan",
3397
- }).start();
3398
- }
3399
- await execAsync(`teammates-recall sync --dir "${this.teammatesDir}"`);
3400
- if (syncSpinner)
3401
- syncSpinner.succeed(`${name}: index synced`);
3402
- if (this.chatView) {
3403
- this.chatView.setProgress(null);
3404
- this.feedLine(tp.success(` ✔ ${name}: index synced`));
3405
- }
3175
+ // Sync recall index for this teammate (bundled library call)
3176
+ try {
3177
+ if (this.chatView) {
3178
+ this.chatView.setProgress(`Syncing ${name} index...`);
3179
+ this.refreshView();
3406
3180
  }
3407
- catch {
3408
- /* sync failed — non-fatal */
3181
+ let syncSpinner = null;
3182
+ if (!this.chatView) {
3183
+ syncSpinner = ora({
3184
+ text: `Syncing ${name} index...`,
3185
+ color: "cyan",
3186
+ }).start();
3409
3187
  }
3188
+ await syncRecallIndex(this.teammatesDir, name);
3189
+ if (syncSpinner)
3190
+ syncSpinner.succeed(`${name}: index synced`);
3191
+ if (this.chatView) {
3192
+ this.chatView.setProgress(null);
3193
+ this.feedLine(tp.success(` ✔ ${name}: index synced`));
3194
+ }
3195
+ }
3196
+ catch {
3197
+ /* sync failed — non-fatal */
3198
+ }
3199
+ // Queue wisdom distillation agent task
3200
+ try {
3201
+ const teammateDir = join(this.teammatesDir, name);
3202
+ const wisdomPrompt = await buildWisdomPrompt(teammateDir, name);
3203
+ if (wisdomPrompt) {
3204
+ this.taskQueue.push({
3205
+ type: "agent",
3206
+ teammate: name,
3207
+ task: wisdomPrompt,
3208
+ });
3209
+ if (this.chatView)
3210
+ this.feedLine(tp.muted(` ↻ ${name}: queued wisdom distillation`));
3211
+ }
3212
+ }
3213
+ catch {
3214
+ /* wisdom prompt build failed — non-fatal */
3410
3215
  }
3411
3216
  }
3412
3217
  catch (err) {
@@ -3503,9 +3308,9 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3503
3308
  const fullPath = join(dir, entry.name);
3504
3309
  if (entry.isDirectory()) {
3505
3310
  await this.cleanOldTempFiles(fullPath, maxAgeMs);
3506
- // Remove dir if now empty — but skip "sessions" which is structural
3507
- // and may be recreated concurrently by startSession().
3508
- if (entry.name !== "sessions") {
3311
+ // Remove dir if now empty — but skip structural dirs that are
3312
+ // recreated concurrently (sessions by startSession, debug by writeDebugEntry).
3313
+ if (entry.name !== "sessions" && entry.name !== "debug") {
3509
3314
  const remaining = await readdir(fullPath).catch(() => [""]);
3510
3315
  if (remaining.length === 0)
3511
3316
  await rm(fullPath, { recursive: true }).catch(() => { });
@@ -3520,8 +3325,16 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3520
3325
  }
3521
3326
  }
3522
3327
  async startupMaintenance() {
3523
- // Clean up .teammates/.tmp files older than 1 week
3524
3328
  const tmpDir = join(this.teammatesDir, ".tmp");
3329
+ // Clean up debug log files older than 1 day
3330
+ const debugDir = join(tmpDir, "debug");
3331
+ try {
3332
+ await this.cleanOldTempFiles(debugDir, 24 * 60 * 60 * 1000);
3333
+ }
3334
+ catch {
3335
+ /* debug dir may not exist yet — non-fatal */
3336
+ }
3337
+ // Clean up other .tmp files older than 1 week
3525
3338
  try {
3526
3339
  await this.cleanOldTempFiles(tmpDir, 7 * 24 * 60 * 60 * 1000);
3527
3340
  }
@@ -3533,8 +3346,6 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3533
3346
  .filter((n) => n !== this.adapterName);
3534
3347
  if (teammates.length === 0)
3535
3348
  return;
3536
- // Check if recall is installed
3537
- const recallInstalled = this.isServiceInstalled("recall");
3538
3349
  // 1. Check each teammate for stale daily logs (older than 7 days)
3539
3350
  const oneWeekAgo = new Date();
3540
3351
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
@@ -3564,14 +3375,12 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3564
3375
  await this.runCompact(name);
3565
3376
  }
3566
3377
  }
3567
- // 2. Sync recall indexes if installed
3568
- if (recallInstalled) {
3569
- try {
3570
- await execAsync(`teammates-recall sync --dir "${this.teammatesDir}"`);
3571
- }
3572
- catch {
3573
- /* sync failed — non-fatal */
3574
- }
3378
+ // 2. Sync recall indexes (bundled library call)
3379
+ try {
3380
+ await syncRecallIndex(this.teammatesDir);
3381
+ }
3382
+ catch {
3383
+ /* sync failed — non-fatal */
3575
3384
  }
3576
3385
  }
3577
3386
  async cmdCopy() {