@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 +12 -6
- package/dist/adapter.d.ts +20 -0
- package/dist/adapter.js +63 -5
- package/dist/adapters/cli-proxy.js +34 -5
- package/dist/adapters/copilot.js +20 -1
- package/dist/cli.js +192 -383
- package/dist/compact.d.ts +7 -0
- package/dist/compact.js +97 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/template/TEMPLATE.md +1 -1
- package/template/example/SOUL.md +1 -1
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
|
|
132
|
-
2. The prompt
|
|
133
|
-
3. The
|
|
134
|
-
4.
|
|
135
|
-
5.
|
|
136
|
-
6.
|
|
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(
|
|
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
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 ?? []),
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -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 {
|