@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 +212 -0
- package/dist/adapter.d.ts +70 -0
- package/dist/adapter.js +159 -0
- package/dist/adapter.test.d.ts +1 -0
- package/dist/adapter.test.js +145 -0
- package/dist/adapters/cli-proxy.d.ts +74 -0
- package/dist/adapters/cli-proxy.js +463 -0
- package/dist/adapters/codex.d.ts +50 -0
- package/dist/adapters/codex.js +213 -0
- package/dist/adapters/echo.d.ts +13 -0
- package/dist/adapters/echo.js +24 -0
- package/dist/adapters/echo.test.d.ts +1 -0
- package/dist/adapters/echo.test.js +40 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +1529 -0
- package/dist/dropdown.d.ts +23 -0
- package/dist/dropdown.js +80 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/onboard.d.ts +18 -0
- package/dist/onboard.js +199 -0
- package/dist/orchestrator.d.ts +70 -0
- package/dist/orchestrator.js +240 -0
- package/dist/orchestrator.test.d.ts +1 -0
- package/dist/orchestrator.test.js +245 -0
- package/dist/registry.d.ts +24 -0
- package/dist/registry.js +145 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +171 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +4 -0
- package/package.json +42 -0
- package/template/CROSS-TEAM.md +31 -0
- package/template/PROTOCOL.md +99 -0
- package/template/README.md +48 -0
- package/template/TEMPLATE.md +109 -0
- package/template/example/MEMORIES.md +26 -0
- package/template/example/SOUL.md +81 -0
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;
|
package/dist/adapter.js
ADDED
|
@@ -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
|
+
}
|