@teammates/cli 0.1.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 ADDED
@@ -0,0 +1,212 @@
1
+ # @teammates/cli
2
+
3
+ Agent-agnostic CLI orchestrator for teammates. Routes tasks to teammates, manages handoffs, and plugs into any coding agent backend.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ cd cli
9
+ npm install
10
+ npm run build
11
+ ```
12
+
13
+ Then launch a session with your preferred agent:
14
+
15
+ ```bash
16
+ teammates claude # Claude Code
17
+ teammates codex # OpenAI Codex
18
+ teammates aider # Aider
19
+ teammates echo # Test adapter (no external agent)
20
+ ```
21
+
22
+ The CLI auto-discovers your `.teammates/` directory by walking up from the current working directory.
23
+
24
+ ## Usage
25
+
26
+ ```
27
+ teammates <agent> [options] [-- agent-flags...]
28
+ ```
29
+
30
+ ### Options
31
+
32
+ | Flag | Description |
33
+ |---|---|
34
+ | `--model <model>` | Override the agent's model |
35
+ | `--dir <path>` | Override `.teammates/` directory location |
36
+ | `--help` | Show usage information |
37
+
38
+ Any arguments after the agent name are passed through to the underlying agent CLI.
39
+
40
+ ## In-Session Commands
41
+
42
+ Once inside the REPL, you can interact with teammates using `@mentions`, `/commands`, or bare text (auto-routed).
43
+
44
+ ### Task Assignment
45
+
46
+ | Input | Behavior |
47
+ |---|---|
48
+ | `@beacon fix the search index` | Assign directly to a teammate |
49
+ | `fix the search index` | Auto-route to the best teammate based on keywords |
50
+ | `/route fix the search index` | Explicitly auto-route |
51
+
52
+ ### Slash Commands
53
+
54
+ | Command | Aliases | Description |
55
+ |---|---|---|
56
+ | `/route <task>` | `/r` | Auto-route a task to the best teammate |
57
+ | `/status` | `/s` | Show teammate roster and session status |
58
+ | `/teammates` | `/team`, `/t` | List all teammates and their roles |
59
+ | `/log [teammate]` | `/l` | Show the last task result (optionally for a specific teammate) |
60
+ | `/debug [teammate]` | `/raw` | Show raw agent output from the last task |
61
+ | `/queue @teammate <task>` | `/qu` | Add a task to the background queue |
62
+ | `/queue` | `/qu` | Show the current queue |
63
+ | `/cancel <n>` | | Cancel a queued task by number |
64
+ | `/install <service>` | | Install an optional service (e.g. `recall`) |
65
+ | `/clear` | `/cls`, `/reset` | Clear conversation history, reset all sessions, and reprint banner |
66
+ | `/help` | `/h`, `/?` | Show available commands |
67
+ | `/exit` | `/q`, `/quit` | Exit the session |
68
+
69
+ ### Autocomplete
70
+
71
+ - Type `/` to see a command wordwheel — arrow keys to navigate, `Tab` to accept
72
+ - Type `@` anywhere in a line to autocomplete teammate names
73
+ - Command arguments that take teammate names also autocomplete (e.g. `/log b` → `/log beacon`)
74
+
75
+ ## Task Queue
76
+
77
+ Queue multiple tasks to run sequentially in the background while the REPL stays responsive:
78
+
79
+ ```
80
+ /queue @beacon update the search index
81
+ /queue @scribe update the onboarding docs
82
+ /queue # show queue status
83
+ /cancel 2 # cancel a queued task
84
+ ```
85
+
86
+ Queued tasks drain one at a time. If a handoff requires approval, the queue pauses until you respond.
87
+
88
+ ## Handoffs
89
+
90
+ When a teammate finishes a task, it may propose a handoff to another teammate. The CLI presents a menu:
91
+
92
+ ```
93
+ 1) Approve — execute the handoff
94
+ 2) Always approve — auto-approve all future handoffs this session
95
+ 3) Reject — decline the handoff
96
+ ```
97
+
98
+ Handoff details (task, changed files, acceptance criteria, open questions) are displayed before you choose.
99
+
100
+ ## Conversation History
101
+
102
+ The CLI maintains a rolling conversation history (last 10 exchanges) that is passed as context to each task. This lets teammates reference prior work in the session without re-reading files.
103
+
104
+ ## Agent Adapters
105
+
106
+ The CLI uses a generic adapter interface to support any coding agent. Each adapter spawns the agent as a subprocess and streams its output.
107
+
108
+ ### Built-in Presets
109
+
110
+ | Preset | Command | Notes |
111
+ |---|---|---|
112
+ | `claude` | `claude -p --verbose` | Requires `claude` on PATH |
113
+ | `codex` | `codex exec` | Requires `codex` on PATH |
114
+ | `aider` | `aider --message-file` | Requires `aider` on PATH |
115
+ | `echo` | (in-process) | Test adapter — echoes prompts, no external agent |
116
+
117
+ ### How Adapters Work
118
+
119
+ 1. The orchestrator builds a full prompt (identity + memory + roster + task)
120
+ 2. The prompt is written to a temp file
121
+ 3. The agent CLI is spawned with the prompt
122
+ 4. stdout/stderr are captured for result parsing
123
+ 5. The output is parsed for structured JSON result/handoff blocks
124
+ 6. Temp files are cleaned up
125
+
126
+ ### Writing a Custom Adapter
127
+
128
+ Implement the `AgentAdapter` interface:
129
+
130
+ ```typescript
131
+ import type { AgentAdapter } from "./adapter.js";
132
+ import type { TeammateConfig, TaskResult } from "./types.js";
133
+
134
+ class MyAdapter implements AgentAdapter {
135
+ readonly name = "my-agent";
136
+
137
+ async startSession(teammate: TeammateConfig): Promise<string> {
138
+ return `my-agent-${teammate.name}`;
139
+ }
140
+
141
+ async executeTask(
142
+ sessionId: string,
143
+ teammate: TeammateConfig,
144
+ prompt: string
145
+ ): Promise<TaskResult> {
146
+ // Call your agent and return results
147
+ }
148
+ }
149
+ ```
150
+
151
+ Or add a preset to `cli-proxy.ts` for any CLI agent that accepts a prompt and runs to completion.
152
+
153
+ ## Architecture
154
+
155
+ ```
156
+ cli/src/
157
+ cli.ts # Entry point, REPL, slash commands, wordwheel UI
158
+ orchestrator.ts # Task routing, handoff chains, session management
159
+ adapter.ts # AgentAdapter interface, prompt builder, handoff formatting
160
+ registry.ts # Discovers teammates from .teammates/, loads SOUL.md + memory
161
+ types.ts # Core types (TeammateConfig, TaskResult, HandoffEnvelope)
162
+ dropdown.ts # Terminal dropdown/wordwheel widget
163
+ adapters/
164
+ cli-proxy.ts # Generic subprocess adapter with agent presets
165
+ echo.ts # Test adapter (no-op)
166
+ ```
167
+
168
+ ### Output Protocol
169
+
170
+ Agents are instructed to end their response with a structured JSON block:
171
+
172
+ ```json
173
+ { "result": { "summary": "...", "changedFiles": ["..."] } }
174
+ ```
175
+
176
+ Or for handoffs:
177
+
178
+ ```json
179
+ { "handoff": { "to": "teammate", "task": "...", "context": "..." } }
180
+ ```
181
+
182
+ The CLI parses the last JSON fence in the output. If no structured block is found, it falls back to scraping file paths and summaries from freeform output.
183
+
184
+ ## Testing
185
+
186
+ Run the test suite:
187
+
188
+ ```bash
189
+ cd cli
190
+ npm test
191
+ ```
192
+
193
+ Run tests in watch mode during development:
194
+
195
+ ```bash
196
+ npm run test:watch
197
+ ```
198
+
199
+ Tests use [Vitest](https://vitest.dev/) and cover the core modules:
200
+
201
+ | File | Covers |
202
+ |---|---|
203
+ | `src/adapter.test.ts` | `buildTeammatePrompt`, `formatHandoffContext` |
204
+ | `src/orchestrator.test.ts` | Task routing, assignment, handoff chains, cycle detection, reset |
205
+ | `src/registry.test.ts` | Teammate discovery, SOUL.md parsing (role, ownership), daily logs |
206
+ | `src/adapters/echo.test.ts` | Echo adapter session and task execution |
207
+
208
+ ## Requirements
209
+
210
+ - Node.js >= 20
211
+ - A `.teammates/` directory in your project (see [ONBOARDING.md](../ONBOARDING.md))
212
+ - The agent CLI on your PATH (for non-echo adapters)
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Agent adapter interface.
3
+ *
4
+ * Implement this to plug any coding agent into the teammates CLI.
5
+ * Each adapter wraps a specific agent backend (Codex, Claude Code, Cursor, etc.)
6
+ * and translates between the orchestrator's protocol and the agent's native API.
7
+ */
8
+ import type { TeammateConfig, TaskResult } from "./types.js";
9
+ export interface AgentAdapter {
10
+ /** Human-readable name of the agent backend (e.g. "codex", "claude-code") */
11
+ readonly name: string;
12
+ /**
13
+ * Start a new session for a teammate.
14
+ * Returns a session/thread ID for continuity.
15
+ */
16
+ startSession(teammate: TeammateConfig): Promise<string>;
17
+ /**
18
+ * Send a task prompt to a teammate's session.
19
+ * The adapter hydrates the prompt with identity, memory, and handoff context.
20
+ */
21
+ executeTask(sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
22
+ /**
23
+ * Resume an existing session (for agents that support continuity).
24
+ * Falls back to startSession if not implemented.
25
+ */
26
+ resumeSession?(teammate: TeammateConfig, sessionId: string): Promise<string>;
27
+ /** Clean up a session. */
28
+ destroySession?(sessionId: string): Promise<void>;
29
+ /**
30
+ * Quick routing call — ask the agent which teammate should handle a task.
31
+ * Returns a teammate name. The agent can return its own name if it should handle it.
32
+ */
33
+ routeTask?(task: string, roster: RosterEntry[]): Promise<string | null>;
34
+ }
35
+ /** Minimal teammate info for the roster section of a prompt. */
36
+ export interface RosterEntry {
37
+ name: string;
38
+ role: string;
39
+ ownership: {
40
+ primary: string[];
41
+ secondary: string[];
42
+ };
43
+ }
44
+ /** A service that's been installed and is available to teammates. */
45
+ export interface InstalledService {
46
+ name: string;
47
+ description: string;
48
+ usage: string;
49
+ }
50
+ /**
51
+ * Build the full prompt for a teammate session.
52
+ * Includes identity, memory, roster, output protocol, and the task.
53
+ */
54
+ export declare function buildTeammatePrompt(teammate: TeammateConfig, taskPrompt: string, options?: {
55
+ handoffContext?: string;
56
+ roster?: RosterEntry[];
57
+ services?: InstalledService[];
58
+ sessionFile?: string;
59
+ }): string;
60
+ /**
61
+ * Format a handoff envelope into a human-readable context string.
62
+ */
63
+ export declare function formatHandoffContext(envelope: {
64
+ from: string;
65
+ task: string;
66
+ changedFiles?: string[];
67
+ acceptanceCriteria?: string[];
68
+ openQuestions?: string[];
69
+ context?: string;
70
+ }): string;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Agent adapter interface.
3
+ *
4
+ * Implement this to plug any coding agent into the teammates CLI.
5
+ * Each adapter wraps a specific agent backend (Codex, Claude Code, Cursor, etc.)
6
+ * and translates between the orchestrator's protocol and the agent's native API.
7
+ */
8
+ /**
9
+ * Build the full prompt for a teammate session.
10
+ * Includes identity, memory, roster, output protocol, and the task.
11
+ */
12
+ export function buildTeammatePrompt(teammate, taskPrompt, options) {
13
+ const parts = [];
14
+ // ── Identity ──────────────────────────────────────────────────────
15
+ parts.push(`# You are ${teammate.name}\n`);
16
+ parts.push(teammate.soul);
17
+ parts.push("\n---\n");
18
+ // ── Memories ──────────────────────────────────────────────────────
19
+ if (teammate.memories.trim()) {
20
+ parts.push("## Your Memories\n");
21
+ parts.push(teammate.memories);
22
+ parts.push("\n---\n");
23
+ }
24
+ if (teammate.dailyLogs.length > 0) {
25
+ parts.push("## Recent Daily Logs\n");
26
+ for (const log of teammate.dailyLogs.slice(0, 3)) {
27
+ parts.push(`### ${log.date}\n${log.content}\n`);
28
+ }
29
+ parts.push("\n---\n");
30
+ }
31
+ // ── Team roster ───────────────────────────────────────────────────
32
+ if (options?.roster && options.roster.length > 0) {
33
+ parts.push("## Your Team\n");
34
+ parts.push("These are the other teammates you can hand off work to:\n");
35
+ for (const t of options.roster) {
36
+ if (t.name === teammate.name)
37
+ continue;
38
+ const owns = t.ownership.primary.length > 0
39
+ ? ` — owns: ${t.ownership.primary.join(", ")}`
40
+ : "";
41
+ parts.push(`- **@${t.name}**: ${t.role}${owns}`);
42
+ }
43
+ parts.push("\n---\n");
44
+ }
45
+ // ── Installed services ──────────────────────────────────────────────
46
+ if (options?.services && options.services.length > 0) {
47
+ parts.push("## Available Services\n");
48
+ parts.push("These services are installed and available for you to use:\n");
49
+ for (const svc of options.services) {
50
+ parts.push(`### ${svc.name}\n`);
51
+ parts.push(svc.description);
52
+ parts.push(`\n**Usage:** \`${svc.usage}\`\n`);
53
+ }
54
+ parts.push("\n---\n");
55
+ }
56
+ // ── Handoff context (if this task came from another teammate) ─────
57
+ if (options?.handoffContext) {
58
+ parts.push("## Handoff Context\n");
59
+ parts.push(options.handoffContext);
60
+ parts.push("\n---\n");
61
+ }
62
+ // ── Session state ────────────────────────────────────────────────
63
+ if (options?.sessionFile) {
64
+ parts.push("## Session State\n");
65
+ parts.push(`Your session file is at: \`${options.sessionFile}\`
66
+
67
+ **Read this file first** — it contains context from your prior tasks in this session.
68
+
69
+ **Before returning your result**, append a brief entry to this file with:
70
+ - What you did
71
+ - Key decisions made
72
+ - Files changed
73
+ - Anything the next task should know
74
+
75
+ This is how you maintain continuity across tasks. Always read it, always update it.
76
+ `);
77
+ parts.push("\n---\n");
78
+ }
79
+ // ── Memory updates ─────────────────────────────────────────────────
80
+ const today = new Date().toISOString().slice(0, 10);
81
+ parts.push("## Memory Updates\n");
82
+ parts.push(`**Before returning your result**, update your memory files:
83
+
84
+ 1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist.
85
+ - What you did
86
+ - Key decisions made
87
+ - Files changed
88
+ - Anything the next task should know
89
+
90
+ 2. **MEMORIES.md** — If you learned something durable (a decision, pattern, gotcha, or bug), read \`.teammates/${teammate.name}/MEMORIES.md\`, then write it back with your entry added.
91
+
92
+ These files are your persistent memory. Without them, your next session starts from scratch.
93
+ `);
94
+ parts.push("\n---\n");
95
+ // ── Output protocol ───────────────────────────────────────────────
96
+ parts.push("## Output Protocol\n");
97
+ parts.push(`When you finish, you MUST end your response with exactly one of these two blocks:
98
+
99
+ ### Option 1: Direct response
100
+
101
+ If you can complete the task yourself, do the work and then end with:
102
+
103
+ \`\`\`json
104
+ { "result": { "summary": "<one-line summary of what you did>", "changedFiles": ["<file>", ...] } }
105
+ \`\`\`
106
+
107
+ ### Option 2: Handoff
108
+
109
+ If the task (or part of it) belongs to another teammate, end with:
110
+
111
+ \`\`\`json
112
+ { "handoff": { "to": "<teammate>", "task": "<specific request for them>", "changedFiles": ["<files you changed, if any>"], "context": "<any context they need>" } }
113
+ \`\`\`
114
+
115
+ You may also write a task file (e.g. \`.teammates/tasks/<name>.md\`) with detailed instructions and reference it in the handoff:
116
+
117
+ \`\`\`json
118
+ { "handoff": { "to": "<teammate>", "task": "See .teammates/tasks/<name>.md", "changedFiles": [".teammates/tasks/<name>.md"] } }
119
+ \`\`\`
120
+
121
+ Rules:
122
+ - Always include exactly one JSON block at the end — either \`result\` or \`handoff\`.
123
+ - Only hand off to teammates listed in "Your Team" above.
124
+ - Do as much of the work as you can before handing off.
125
+ - If the task is outside everyone's ownership, do your best and return a result.
126
+ `);
127
+ parts.push("\n---\n");
128
+ // ── Task ──────────────────────────────────────────────────────────
129
+ parts.push("## Task\n");
130
+ parts.push(taskPrompt);
131
+ return parts.join("\n");
132
+ }
133
+ /**
134
+ * Format a handoff envelope into a human-readable context string.
135
+ */
136
+ export function formatHandoffContext(envelope) {
137
+ const lines = [];
138
+ lines.push(`**Handed off from:** ${envelope.from}`);
139
+ lines.push(`**Task:** ${envelope.task}`);
140
+ if (envelope.changedFiles?.length) {
141
+ lines.push("\n**Changed files:**");
142
+ for (const f of envelope.changedFiles)
143
+ lines.push(`- ${f}`);
144
+ }
145
+ if (envelope.acceptanceCriteria?.length) {
146
+ lines.push("\n**Acceptance criteria:**");
147
+ for (const c of envelope.acceptanceCriteria)
148
+ lines.push(`- ${c}`);
149
+ }
150
+ if (envelope.openQuestions?.length) {
151
+ lines.push("\n**Open questions:**");
152
+ for (const q of envelope.openQuestions)
153
+ lines.push(`- ${q}`);
154
+ }
155
+ if (envelope.context) {
156
+ lines.push(`\n**Additional context:**\n${envelope.context}`);
157
+ }
158
+ return lines.join("\n");
159
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildTeammatePrompt, formatHandoffContext } from "./adapter.js";
3
+ function makeConfig(overrides) {
4
+ return {
5
+ name: "beacon",
6
+ role: "Platform engineer.",
7
+ soul: "# Beacon\n\nBeacon owns the recall package.",
8
+ memories: "",
9
+ dailyLogs: [],
10
+ ownership: { primary: ["recall/src/**"], secondary: [] },
11
+ ...overrides,
12
+ };
13
+ }
14
+ describe("buildTeammatePrompt", () => {
15
+ it("includes identity header", () => {
16
+ const prompt = buildTeammatePrompt(makeConfig(), "do the thing");
17
+ expect(prompt).toContain("# You are beacon");
18
+ });
19
+ it("includes soul content", () => {
20
+ const prompt = buildTeammatePrompt(makeConfig(), "do the thing");
21
+ expect(prompt).toContain("Beacon owns the recall package");
22
+ });
23
+ it("includes the task", () => {
24
+ const prompt = buildTeammatePrompt(makeConfig(), "fix the bug");
25
+ expect(prompt).toContain("## Task");
26
+ expect(prompt).toContain("fix the bug");
27
+ });
28
+ it("includes output protocol", () => {
29
+ const prompt = buildTeammatePrompt(makeConfig(), "task");
30
+ expect(prompt).toContain("## Output Protocol");
31
+ expect(prompt).toContain('"result"');
32
+ expect(prompt).toContain('"handoff"');
33
+ });
34
+ it("includes memory updates section", () => {
35
+ const prompt = buildTeammatePrompt(makeConfig(), "task");
36
+ expect(prompt).toContain("## Memory Updates");
37
+ expect(prompt).toContain(".teammates/beacon/memory/");
38
+ });
39
+ it("skips memories section when empty", () => {
40
+ const prompt = buildTeammatePrompt(makeConfig({ memories: "" }), "task");
41
+ expect(prompt).not.toContain("## Your Memories");
42
+ });
43
+ it("includes memories when present", () => {
44
+ const prompt = buildTeammatePrompt(makeConfig({ memories: "Some important memory" }), "task");
45
+ expect(prompt).toContain("## Your Memories");
46
+ expect(prompt).toContain("Some important memory");
47
+ });
48
+ it("includes daily logs (up to 3)", () => {
49
+ const logs = [
50
+ { date: "2026-03-13", content: "Did stuff today" },
51
+ { date: "2026-03-12", content: "Did stuff yesterday" },
52
+ { date: "2026-03-11", content: "Day before" },
53
+ { date: "2026-03-10", content: "Should be excluded" },
54
+ ];
55
+ const prompt = buildTeammatePrompt(makeConfig({ dailyLogs: logs }), "task");
56
+ expect(prompt).toContain("## Recent Daily Logs");
57
+ expect(prompt).toContain("2026-03-13");
58
+ expect(prompt).toContain("2026-03-12");
59
+ expect(prompt).toContain("2026-03-11");
60
+ expect(prompt).not.toContain("2026-03-10");
61
+ expect(prompt).not.toContain("Should be excluded");
62
+ });
63
+ it("includes roster excluding self", () => {
64
+ const roster = [
65
+ { name: "beacon", role: "Platform engineer.", ownership: { primary: [], secondary: [] } },
66
+ { name: "scribe", role: "Documentation writer.", ownership: { primary: ["docs/**"], secondary: [] } },
67
+ ];
68
+ const prompt = buildTeammatePrompt(makeConfig(), "task", { roster });
69
+ expect(prompt).toContain("## Your Team");
70
+ expect(prompt).toContain("@scribe");
71
+ expect(prompt).toContain("Documentation writer.");
72
+ // Should not list self in roster
73
+ expect(prompt).not.toContain("@beacon");
74
+ });
75
+ it("includes handoff context when provided", () => {
76
+ const prompt = buildTeammatePrompt(makeConfig(), "task", {
77
+ handoffContext: "Handed off from scribe with files changed",
78
+ });
79
+ expect(prompt).toContain("## Handoff Context");
80
+ expect(prompt).toContain("Handed off from scribe");
81
+ });
82
+ it("includes session file when provided", () => {
83
+ const prompt = buildTeammatePrompt(makeConfig(), "task", {
84
+ sessionFile: "/tmp/beacon-session.md",
85
+ });
86
+ expect(prompt).toContain("## Session State");
87
+ expect(prompt).toContain("/tmp/beacon-session.md");
88
+ });
89
+ });
90
+ describe("formatHandoffContext", () => {
91
+ it("formats basic handoff", () => {
92
+ const result = formatHandoffContext({
93
+ from: "beacon",
94
+ task: "update the docs",
95
+ });
96
+ expect(result).toContain("**Handed off from:** beacon");
97
+ expect(result).toContain("**Task:** update the docs");
98
+ });
99
+ it("includes changed files", () => {
100
+ const result = formatHandoffContext({
101
+ from: "beacon",
102
+ task: "review",
103
+ changedFiles: ["src/foo.ts", "src/bar.ts"],
104
+ });
105
+ expect(result).toContain("**Changed files:**");
106
+ expect(result).toContain("- src/foo.ts");
107
+ expect(result).toContain("- src/bar.ts");
108
+ });
109
+ it("includes acceptance criteria", () => {
110
+ const result = formatHandoffContext({
111
+ from: "beacon",
112
+ task: "review",
113
+ acceptanceCriteria: ["Tests pass", "No lint errors"],
114
+ });
115
+ expect(result).toContain("**Acceptance criteria:**");
116
+ expect(result).toContain("- Tests pass");
117
+ expect(result).toContain("- No lint errors");
118
+ });
119
+ it("includes open questions", () => {
120
+ const result = formatHandoffContext({
121
+ from: "beacon",
122
+ task: "review",
123
+ openQuestions: ["Should we rename?"],
124
+ });
125
+ expect(result).toContain("**Open questions:**");
126
+ expect(result).toContain("- Should we rename?");
127
+ });
128
+ it("includes additional context", () => {
129
+ const result = formatHandoffContext({
130
+ from: "beacon",
131
+ task: "review",
132
+ context: "This is urgent",
133
+ });
134
+ expect(result).toContain("**Additional context:**");
135
+ expect(result).toContain("This is urgent");
136
+ });
137
+ it("omits sections with empty arrays", () => {
138
+ const result = formatHandoffContext({
139
+ from: "beacon",
140
+ task: "review",
141
+ changedFiles: [],
142
+ });
143
+ expect(result).not.toContain("**Changed files:**");
144
+ });
145
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Generic CLI proxy adapter — spawns any coding agent as a subprocess
3
+ * and streams its output live to the user's terminal.
4
+ *
5
+ * Supports any CLI agent that accepts a prompt and runs to completion:
6
+ * claude -p "prompt"
7
+ * codex exec "prompt" --full-auto
8
+ * aider --message "prompt"
9
+ * etc.
10
+ *
11
+ * The adapter:
12
+ * 1. Writes the full prompt (identity + memory + task) to a temp file
13
+ * 2. Spawns the agent with the prompt file
14
+ * 3. Tees stdout/stderr to the user's terminal in real time
15
+ * 4. Captures output for result parsing (changed files, handoff envelopes)
16
+ */
17
+ import type { AgentAdapter, RosterEntry, InstalledService } from "../adapter.js";
18
+ import type { TeammateConfig, TaskResult, SandboxLevel } from "../types.js";
19
+ export interface AgentPreset {
20
+ /** Display name */
21
+ name: string;
22
+ /** Binary / command to spawn */
23
+ command: string;
24
+ /** Build CLI args. `promptFile` is a temp file path, `prompt` is the raw text. */
25
+ buildArgs(ctx: {
26
+ promptFile: string;
27
+ prompt: string;
28
+ }, teammate: TeammateConfig, options: CliProxyOptions): string[];
29
+ /** Extra env vars to set (e.g. FORCE_COLOR) */
30
+ env?: Record<string, string>;
31
+ /** Whether the agent may prompt the user for input (connects stdin) */
32
+ interactive?: boolean;
33
+ /** Whether the command needs shell: true to run */
34
+ shell?: boolean;
35
+ }
36
+ export declare const PRESETS: Record<string, AgentPreset>;
37
+ export interface CliProxyOptions {
38
+ /** Preset name or custom preset */
39
+ preset: string | AgentPreset;
40
+ /** Model override */
41
+ model?: string;
42
+ /** Default sandbox level */
43
+ defaultSandbox?: SandboxLevel;
44
+ /** Timeout in ms (default: 600_000 = 10 min) */
45
+ timeout?: number;
46
+ /** Extra CLI flags appended to the command */
47
+ extraFlags?: string[];
48
+ /** Custom command path override (e.g. "/usr/local/bin/claude") */
49
+ commandPath?: string;
50
+ }
51
+ export declare class CliProxyAdapter implements AgentAdapter {
52
+ readonly name: string;
53
+ /** Team roster — set by the orchestrator so prompts include teammate info. */
54
+ roster: RosterEntry[];
55
+ /** Installed services — set by the CLI so prompts include service info. */
56
+ services: InstalledService[];
57
+ private preset;
58
+ private options;
59
+ /** Session files per teammate — persists state across task invocations. */
60
+ private sessionFiles;
61
+ /** Base directory for session files. */
62
+ private sessionsDir;
63
+ /** Temp prompt files that need cleanup — guards against crashes before finally. */
64
+ private pendingTempFiles;
65
+ constructor(options: CliProxyOptions);
66
+ startSession(teammate: TeammateConfig): Promise<string>;
67
+ executeTask(sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
68
+ routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
69
+ destroySession(sessionId: string): Promise<void>;
70
+ /**
71
+ * Spawn the agent, stream its output live, and capture it.
72
+ */
73
+ private spawnAndProxy;
74
+ }