@teammates/cli 0.2.7 → 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/README.md CHANGED
@@ -128,12 +128,14 @@ The CLI uses a generic adapter interface to support any coding agent. Each adapt
128
128
 
129
129
  ### How Adapters Work
130
130
 
131
- 1. The orchestrator builds a full prompt (identity + memory + roster + task)
132
- 2. The prompt is written to a temp file
133
- 3. The agent CLI is spawned with the prompt
134
- 4. stdout/stderr are captured for result parsing
135
- 5. The output is parsed for embedded handoff blocks
136
- 6. Temp files are cleaned up
131
+ 1. The adapter queries the recall index for relevant memories (automatic, in-process)
132
+ 2. The orchestrator builds a full prompt (SOUL WISDOM recall results → daily logs → weekly summaries → session history → roster → task)
133
+ 3. The prompt is written to a temp file
134
+ 4. The agent CLI is spawned with the prompt
135
+ 5. stdout/stderr are captured for result parsing
136
+ 6. The output is parsed for embedded handoff blocks
137
+ 7. The recall index is synced to pick up any files the agent created/modified
138
+ 8. Temp files are cleaned up
137
139
 
138
140
  ### Writing a Custom Adapter
139
141
 
@@ -214,6 +216,10 @@ Tests use [Vitest](https://vitest.dev/) and cover the core modules:
214
216
  | `src/registry.test.ts` | Teammate discovery, SOUL.md parsing (role, ownership), daily logs |
215
217
  | `src/adapters/echo.test.ts` | Echo adapter session and task execution |
216
218
 
219
+ ## Dependencies
220
+
221
+ - **`@teammates/recall`** — Bundled as a direct dependency. Provides automatic semantic search over teammate memories before every task. No separate installation or configuration needed.
222
+
217
223
  ## Requirements
218
224
 
219
225
  - Node.js >= 20
package/dist/adapter.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * Each adapter wraps a specific agent backend (Codex, Claude Code, Cursor, etc.)
6
6
  * and translates between the orchestrator's protocol and the agent's native API.
7
7
  */
8
+ import { type SearchResult } from "@teammates/recall";
8
9
  import type { TaskResult, TeammateConfig } from "./types.js";
9
10
  export interface AgentAdapter {
10
11
  /** Human-readable name of the agent backend (e.g. "codex", "claude-code") */
@@ -49,6 +50,23 @@ export interface InstalledService {
49
50
  description: string;
50
51
  usage: string;
51
52
  }
53
+ /** Recall search results formatted for prompt injection. */
54
+ export interface RecallContext {
55
+ results: SearchResult[];
56
+ /** Whether the query succeeded (false = index missing or search errored) */
57
+ ok: boolean;
58
+ }
59
+ /**
60
+ * Query the recall index for context relevant to the task prompt.
61
+ * Returns search results that should be injected into the teammate prompt.
62
+ * Skips auto-sync (sync happens after tasks, not before — keeps pre-task fast).
63
+ */
64
+ export declare function queryRecallContext(teammatesDir: string, teammateName: string, taskPrompt: string): Promise<RecallContext>;
65
+ /**
66
+ * Sync the recall index for a teammate (or all teammates).
67
+ * Wrapper around the recall library's Indexer.
68
+ */
69
+ export declare function syncRecallIndex(teammatesDir: string, teammate?: string): Promise<void>;
52
70
  /**
53
71
  * Build the full prompt for a teammate session.
54
72
  * Includes identity, memory, roster, output protocol, and the task.
@@ -58,6 +76,8 @@ export declare function buildTeammatePrompt(teammate: TeammateConfig, taskPrompt
58
76
  roster?: RosterEntry[];
59
77
  services?: InstalledService[];
60
78
  sessionFile?: string;
79
+ sessionContent?: string;
80
+ recallResults?: SearchResult[];
61
81
  }): string;
62
82
  /**
63
83
  * Format a handoff envelope into a human-readable context string.
package/dist/adapter.js CHANGED
@@ -5,6 +5,41 @@
5
5
  * Each adapter wraps a specific agent backend (Codex, Claude Code, Cursor, etc.)
6
6
  * and translates between the orchestrator's protocol and the agent's native API.
7
7
  */
8
+ import { Indexer, search } from "@teammates/recall";
9
+ /**
10
+ * Query the recall index for context relevant to the task prompt.
11
+ * Returns search results that should be injected into the teammate prompt.
12
+ * Skips auto-sync (sync happens after tasks, not before — keeps pre-task fast).
13
+ */
14
+ export async function queryRecallContext(teammatesDir, teammateName, taskPrompt) {
15
+ try {
16
+ const results = await search(taskPrompt, {
17
+ teammatesDir,
18
+ teammate: teammateName,
19
+ maxResults: 5,
20
+ maxChunks: 3,
21
+ maxTokens: 500,
22
+ skipSync: true,
23
+ });
24
+ return { results, ok: true };
25
+ }
26
+ catch {
27
+ return { results: [], ok: false };
28
+ }
29
+ }
30
+ /**
31
+ * Sync the recall index for a teammate (or all teammates).
32
+ * Wrapper around the recall library's Indexer.
33
+ */
34
+ export async function syncRecallIndex(teammatesDir, teammate) {
35
+ const indexer = new Indexer({ teammatesDir });
36
+ if (teammate) {
37
+ await indexer.syncTeammate(teammate);
38
+ }
39
+ else {
40
+ await indexer.syncAll();
41
+ }
42
+ }
8
43
  /**
9
44
  * Build the full prompt for a teammate session.
10
45
  * Includes identity, memory, roster, output protocol, and the task.
@@ -21,6 +56,19 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
21
56
  parts.push(teammate.wisdom);
22
57
  parts.push("\n---\n");
23
58
  }
59
+ // ── Recall results (relevant episodic & semantic memories) ────────
60
+ if (options?.recallResults && options.recallResults.length > 0) {
61
+ parts.push("## Relevant Memories (from recall search)\n");
62
+ parts.push("These memories were retrieved based on relevance to the current task:\n");
63
+ for (const r of options.recallResults) {
64
+ const label = r.contentType
65
+ ? `[${r.contentType}] ${r.uri}`
66
+ : r.uri;
67
+ parts.push(`### ${label}\n${r.text}\n`);
68
+ }
69
+ parts.push("\n---\n");
70
+ }
71
+ // ── Recent daily logs ──────────────────────────────────────────────
24
72
  if (teammate.dailyLogs.length > 0) {
25
73
  parts.push("## Recent Daily Logs\n");
26
74
  for (const log of teammate.dailyLogs.slice(0, 7)) {
@@ -36,6 +84,13 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
36
84
  }
37
85
  parts.push("\n---\n");
38
86
  }
87
+ // ── Session history (prior tasks in this session) ─────────────────
88
+ if (options?.sessionContent?.trim()) {
89
+ parts.push("## Session History\n");
90
+ parts.push("These are entries from your prior tasks in this session:\n");
91
+ parts.push(options.sessionContent);
92
+ parts.push("\n---\n");
93
+ }
39
94
  // ── Team roster ───────────────────────────────────────────────────
40
95
  if (options?.roster && options.roster.length > 0) {
41
96
  parts.push("## Your Team\n");
@@ -72,8 +127,6 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
72
127
  parts.push("## Session State\n");
73
128
  parts.push(`Your session file is at: \`${options.sessionFile}\`
74
129
 
75
- **Read this file first** — it contains context from your prior tasks in this session.
76
-
77
130
  **Before returning your result**, append a brief entry to this file with:
78
131
  - What you did
79
132
  - Key decisions made
@@ -103,8 +156,10 @@ These files are your persistent memory. Without them, your next session starts f
103
156
  `);
104
157
  parts.push("\n---\n");
105
158
  // ── Output protocol ───────────────────────────────────────────────
106
- parts.push("## Output Protocol\n");
107
- parts.push(`Your response is a message. Format it as:
159
+ parts.push("## Output Protocol (CRITICAL)\n");
160
+ parts.push(`**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.
161
+
162
+ Format your response as:
108
163
 
109
164
  \`\`\`
110
165
  TO: user
@@ -123,9 +178,9 @@ TO: user
123
178
  \`\`\`
124
179
 
125
180
  **Rules:**
181
+ - **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.
126
182
  - The \`# Subject\` line is REQUIRED — it becomes the message title.
127
183
  - Always write a substantive body. Never return just the subject.
128
- - **Your final message MUST contain your response text.** Do not end your turn with only tool calls — always finish with a visible message to the user.
129
184
  - Use markdown: headings, lists, code blocks, bold, etc.
130
185
  - Do as much work as you can before handing off.
131
186
  - Only hand off to teammates listed in "Your Team" above.
@@ -140,6 +195,9 @@ TO: user
140
195
  // ── Task ──────────────────────────────────────────────────────────
141
196
  parts.push("## Task\n");
142
197
  parts.push(taskPrompt);
198
+ parts.push("\n---\n");
199
+ // ── Final reminder (last thing the agent reads) ─────────────────
200
+ parts.push("**REMINDER: After completing the task and updating session/memory files, you MUST produce a text response starting with `TO: user`. An empty response is a bug.**");
143
201
  return parts.join("\n");
144
202
  }
145
203
  /**
@@ -16,15 +16,33 @@
16
16
  */
17
17
  import type { AgentAdapter, InstalledService, RosterEntry } from "../adapter.js";
18
18
  import type { SandboxLevel, TaskResult, TeammateConfig } from "../types.js";
19
+ /** Structured result from spawning an agent subprocess. */
20
+ export interface SpawnResult {
21
+ /** Combined stdout + stderr (for backward compat / display) */
22
+ output: string;
23
+ /** stdout only */
24
+ stdout: string;
25
+ /** stderr only */
26
+ stderr: string;
27
+ /** Process exit code (null if killed by signal) */
28
+ exitCode: number | null;
29
+ /** Signal that killed the process (null if exited normally) */
30
+ signal: string | null;
31
+ /** Whether the process was killed by our timeout */
32
+ timedOut: boolean;
33
+ /** Path to the debug log file, if one was written */
34
+ debugFile?: string;
35
+ }
19
36
  export interface AgentPreset {
20
37
  /** Display name */
21
38
  name: string;
22
39
  /** Binary / command to spawn */
23
40
  command: string;
24
- /** Build CLI args. `promptFile` is a temp file path, `prompt` is the raw text. */
41
+ /** Build CLI args. `promptFile` is a temp file path, `prompt` is the raw text, `debugFile` is an optional path for agent debug logs. */
25
42
  buildArgs(ctx: {
26
43
  promptFile: string;
27
44
  prompt: string;
45
+ debugFile?: string;
28
46
  }, teammate: TeammateConfig, options: CliProxyOptions): string[];
29
47
  /** Extra env vars to set (e.g. FORCE_COLOR) */
30
48
  env?: Record<string, string>;
@@ -34,6 +52,8 @@ export interface AgentPreset {
34
52
  shell?: boolean;
35
53
  /** Whether to pipe the prompt via stdin instead of as a CLI argument */
36
54
  stdinPrompt?: boolean;
55
+ /** Whether this preset supports a debug log file (--debug-file) */
56
+ supportsDebugFile?: boolean;
37
57
  /** Optional output parser — transforms raw stdout into clean agent output */
38
58
  parseOutput?(raw: string): string;
39
59
  }
@@ -16,22 +16,26 @@
16
16
  */
17
17
  import { spawn } from "node:child_process";
18
18
  import { randomUUID } from "node:crypto";
19
+ import { mkdirSync } from "node:fs";
19
20
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
20
21
  import { tmpdir } from "node:os";
21
22
  import { join } from "node:path";
22
- import { buildTeammatePrompt } from "../adapter.js";
23
+ import { buildTeammatePrompt, queryRecallContext } from "../adapter.js";
23
24
  export const PRESETS = {
24
25
  claude: {
25
26
  name: "claude",
26
27
  command: "claude",
27
- buildArgs(_ctx, _teammate, options) {
28
+ buildArgs(ctx, _teammate, options) {
28
29
  const args = ["-p", "--verbose", "--dangerously-skip-permissions"];
29
30
  if (options.model)
30
31
  args.push("--model", options.model);
32
+ if (ctx.debugFile)
33
+ args.push("--debug-file", ctx.debugFile);
31
34
  return args;
32
35
  },
33
36
  env: { FORCE_COLOR: "1", CLAUDECODE: "" },
34
37
  stdinPrompt: true,
38
+ supportsDebugFile: true,
35
39
  },
36
40
  codex: {
37
41
  name: "codex",
@@ -136,12 +140,31 @@ export class CliProxyAdapter {
136
140
  // If the teammate has no soul (e.g. the raw agent), skip identity/memory
137
141
  // wrapping but include handoff instructions so it can delegate to teammates
138
142
  const sessionFile = this.sessionFiles.get(teammate.name);
143
+ // Read session file content for injection into the prompt
144
+ let sessionContent;
145
+ if (sessionFile) {
146
+ try {
147
+ sessionContent = await readFile(sessionFile, "utf-8");
148
+ }
149
+ catch {
150
+ // Session file may not exist yet — that's fine
151
+ }
152
+ }
139
153
  let fullPrompt;
140
154
  if (teammate.soul) {
155
+ // Query recall for relevant memories before building prompt
156
+ const teammatesDir = teammate.cwd
157
+ ? join(teammate.cwd, ".teammates")
158
+ : undefined;
159
+ const recall = teammatesDir
160
+ ? await queryRecallContext(teammatesDir, teammate.name, prompt)
161
+ : undefined;
141
162
  fullPrompt = buildTeammatePrompt(teammate, prompt, {
142
163
  roster: this.roster,
143
164
  services: this.services,
144
165
  sessionFile,
166
+ sessionContent,
167
+ recallResults: recall?.results,
145
168
  });
146
169
  }
147
170
  else {
@@ -167,12 +190,20 @@ export class CliProxyAdapter {
167
190
  await writeFile(promptFile, fullPrompt, "utf-8");
168
191
  this.pendingTempFiles.add(promptFile);
169
192
  try {
170
- const rawOutput = await this.spawnAndProxy(teammate, promptFile, fullPrompt);
193
+ const spawn = await this.spawnAndProxy(teammate, promptFile, fullPrompt);
171
194
  const output = this.preset.parseOutput
172
- ? this.preset.parseOutput(rawOutput)
173
- : rawOutput;
195
+ ? this.preset.parseOutput(spawn.output)
196
+ : spawn.output;
174
197
  const teammateNames = this.roster.map((r) => r.name);
175
- return parseResult(teammate.name, output, teammateNames, prompt);
198
+ const result = parseResult(teammate.name, output, teammateNames, prompt);
199
+ result.diagnostics = {
200
+ exitCode: spawn.exitCode,
201
+ signal: spawn.signal,
202
+ stderr: spawn.stderr,
203
+ timedOut: spawn.timedOut,
204
+ debugFile: spawn.debugFile,
205
+ };
206
+ return result;
176
207
  }
177
208
  finally {
178
209
  this.pendingTempFiles.delete(promptFile);
@@ -291,8 +322,21 @@ export class CliProxyAdapter {
291
322
  */
292
323
  spawnAndProxy(teammate, promptFile, fullPrompt) {
293
324
  return new Promise((resolve, reject) => {
325
+ // Always generate a debug log file for presets that support it (e.g. Claude's --debug-file).
326
+ // Written to .teammates/.tmp/debug/ so startup maintenance can clean old logs.
327
+ let debugFile;
328
+ if (this.preset.supportsDebugFile) {
329
+ const debugDir = join(teammate.cwd ?? process.cwd(), ".teammates", ".tmp", "debug");
330
+ try {
331
+ mkdirSync(debugDir, { recursive: true });
332
+ }
333
+ catch {
334
+ /* best effort */
335
+ }
336
+ debugFile = join(debugDir, `agent-${teammate.name}-${Date.now()}.log`);
337
+ }
294
338
  const args = [
295
- ...this.preset.buildArgs({ promptFile, prompt: fullPrompt }, teammate, this.options),
339
+ ...this.preset.buildArgs({ promptFile, prompt: fullPrompt, debugFile }, teammate, this.options),
296
340
  ...(this.options.extraFlags ?? []),
297
341
  ];
298
342
  const command = this.options.commandPath ?? this.preset.command;
@@ -352,12 +396,13 @@ export class CliProxyAdapter {
352
396
  process.stdin.resume();
353
397
  process.stdin.on("data", onUserInput);
354
398
  }
355
- const captured = [];
399
+ const stdoutBufs = [];
400
+ const stderrBufs = [];
356
401
  child.stdout?.on("data", (chunk) => {
357
- captured.push(chunk);
402
+ stdoutBufs.push(chunk);
358
403
  });
359
404
  child.stderr?.on("data", (chunk) => {
360
- captured.push(chunk);
405
+ stderrBufs.push(chunk);
361
406
  });
362
407
  const cleanup = () => {
363
408
  clearTimeout(timeoutTimer);
@@ -367,15 +412,22 @@ export class CliProxyAdapter {
367
412
  process.stdin.removeListener("data", onUserInput);
368
413
  }
369
414
  };
370
- child.on("close", (_code) => {
415
+ child.on("close", (code, signal) => {
371
416
  cleanup();
372
- const output = Buffer.concat(captured).toString("utf-8");
373
- if (killed) {
374
- resolve(`${output}\n\n[TIMEOUT] Agent process killed after ${timeout}ms`);
375
- }
376
- else {
377
- resolve(output);
378
- }
417
+ const stdout = Buffer.concat(stdoutBufs).toString("utf-8");
418
+ const stderr = Buffer.concat(stderrBufs).toString("utf-8");
419
+ const output = stdout + (stderr ? `\n${stderr}` : "");
420
+ resolve({
421
+ output: killed
422
+ ? `${output}\n\n[TIMEOUT] Agent process killed after ${timeout}ms`
423
+ : output,
424
+ stdout,
425
+ stderr,
426
+ exitCode: code,
427
+ signal: signal ?? null,
428
+ timedOut: killed,
429
+ debugFile,
430
+ });
379
431
  });
380
432
  child.on("error", (err) => {
381
433
  cleanup();
@@ -12,7 +12,7 @@
12
12
  import { mkdir, readFile, writeFile } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
  import { approveAll, CopilotClient, } from "@github/copilot-sdk";
15
- import { buildTeammatePrompt } from "../adapter.js";
15
+ import { buildTeammatePrompt, queryRecallContext } from "../adapter.js";
16
16
  import { parseResult } from "./cli-proxy.js";
17
17
  // ─── Adapter ─────────────────────────────────────────────────────────
18
18
  let nextId = 1;
@@ -59,13 +59,32 @@ export class CopilotAdapter {
59
59
  async executeTask(_sessionId, teammate, prompt) {
60
60
  await this.ensureClient(teammate.cwd);
61
61
  const sessionFile = this.sessionFiles.get(teammate.name);
62
+ // Read session file content for injection into the prompt
63
+ let sessionContent;
64
+ if (sessionFile) {
65
+ try {
66
+ sessionContent = await readFile(sessionFile, "utf-8");
67
+ }
68
+ catch {
69
+ // Session file may not exist yet — that's fine
70
+ }
71
+ }
62
72
  // Build the full teammate prompt (identity + memory + task)
63
73
  let fullPrompt;
64
74
  if (teammate.soul) {
75
+ // Query recall for relevant memories before building prompt
76
+ const teammatesDir = teammate.cwd
77
+ ? join(teammate.cwd, ".teammates")
78
+ : undefined;
79
+ const recall = teammatesDir
80
+ ? await queryRecallContext(teammatesDir, teammate.name, prompt)
81
+ : undefined;
65
82
  fullPrompt = buildTeammatePrompt(teammate, prompt, {
66
83
  roster: this.roster,
67
84
  services: this.services,
68
85
  sessionFile,
86
+ sessionContent,
87
+ recallResults: recall?.results,
69
88
  });
70
89
  }
71
90
  else {