@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/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,10 +16,11 @@
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",
@@ -139,12 +140,31 @@ export class CliProxyAdapter {
139
140
  // If the teammate has no soul (e.g. the raw agent), skip identity/memory
140
141
  // wrapping but include handoff instructions so it can delegate to teammates
141
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
+ }
142
153
  let fullPrompt;
143
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;
144
162
  fullPrompt = buildTeammatePrompt(teammate, prompt, {
145
163
  roster: this.roster,
146
164
  services: this.services,
147
165
  sessionFile,
166
+ sessionContent,
167
+ recallResults: recall?.results,
148
168
  });
149
169
  }
150
170
  else {
@@ -302,10 +322,19 @@ export class CliProxyAdapter {
302
322
  */
303
323
  spawnAndProxy(teammate, promptFile, fullPrompt) {
304
324
  return new Promise((resolve, reject) => {
305
- // Generate a debug log file path if the preset supports it
306
- const debugFile = this.preset.supportsDebugFile
307
- ? join(tmpdir(), `teammates-debug-${teammate.name}-${Date.now()}.log`)
308
- : undefined;
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
+ }
309
338
  const args = [
310
339
  ...this.preset.buildArgs({ promptFile, prompt: fullPrompt, debugFile }, teammate, this.options),
311
340
  ...(this.options.extraFlags ?? []),
@@ -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 {