@teammates/cli 0.1.0 → 0.2.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 +31 -22
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +68 -56
- package/dist/adapter.test.js +34 -21
- package/dist/adapters/cli-proxy.d.ts +11 -4
- package/dist/adapters/cli-proxy.js +176 -162
- package/dist/adapters/copilot.d.ts +50 -0
- package/dist/adapters/copilot.js +210 -0
- package/dist/adapters/echo.d.ts +2 -2
- package/dist/adapters/echo.js +2 -1
- package/dist/adapters/echo.test.js +4 -2
- package/dist/cli-utils.d.ts +21 -0
- package/dist/cli-utils.js +74 -0
- package/dist/cli-utils.test.d.ts +1 -0
- package/dist/cli-utils.test.js +179 -0
- package/dist/cli.js +3160 -961
- package/dist/compact.d.ts +39 -0
- package/dist/compact.js +269 -0
- package/dist/compact.test.d.ts +1 -0
- package/dist/compact.test.js +198 -0
- package/dist/console/ansi.d.ts +18 -0
- package/dist/console/ansi.js +20 -0
- package/dist/console/ansi.test.d.ts +1 -0
- package/dist/console/ansi.test.js +50 -0
- package/dist/console/dropdown.d.ts +23 -0
- package/dist/console/dropdown.js +63 -0
- package/dist/console/file-drop.d.ts +59 -0
- package/dist/console/file-drop.js +186 -0
- package/dist/console/file-drop.test.d.ts +1 -0
- package/dist/console/file-drop.test.js +145 -0
- package/dist/console/index.d.ts +22 -0
- package/dist/console/index.js +23 -0
- package/dist/console/interactive-readline.d.ts +65 -0
- package/dist/console/interactive-readline.js +132 -0
- package/dist/console/markdown-table.d.ts +17 -0
- package/dist/console/markdown-table.js +270 -0
- package/dist/console/markdown-table.test.d.ts +1 -0
- package/dist/console/markdown-table.test.js +130 -0
- package/dist/console/mutable-output.d.ts +21 -0
- package/dist/console/mutable-output.js +51 -0
- package/dist/console/paste-handler.d.ts +63 -0
- package/dist/console/paste-handler.js +177 -0
- package/dist/console/prompt-box.d.ts +55 -0
- package/dist/console/prompt-box.js +120 -0
- package/dist/console/prompt-input.d.ts +136 -0
- package/dist/console/prompt-input.js +618 -0
- package/dist/console/startup.d.ts +20 -0
- package/dist/console/startup.js +138 -0
- package/dist/console/startup.test.d.ts +1 -0
- package/dist/console/startup.test.js +41 -0
- package/dist/console/wordwheel.d.ts +75 -0
- package/dist/console/wordwheel.js +123 -0
- package/dist/dropdown.js +4 -21
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/onboard.d.ts +24 -0
- package/dist/onboard.js +174 -11
- package/dist/orchestrator.d.ts +8 -11
- package/dist/orchestrator.js +33 -81
- package/dist/orchestrator.test.js +59 -79
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +56 -12
- package/dist/registry.test.js +57 -13
- package/dist/theme.d.ts +56 -0
- package/dist/theme.js +54 -0
- package/dist/types.d.ts +18 -13
- package/package.json +8 -3
- package/template/CROSS-TEAM.md +2 -2
- package/template/PROTOCOL.md +72 -15
- package/template/README.md +2 -2
- package/template/TEMPLATE.md +118 -15
- package/template/example/SOUL.md +2 -1
- package/template/example/WISDOM.md +9 -0
- package/dist/adapters/codex.d.ts +0 -50
- package/dist/adapters/codex.js +0 -213
- package/template/example/MEMORIES.md +0 -26
|
@@ -15,44 +15,66 @@
|
|
|
15
15
|
* 4. Captures output for result parsing (changed files, handoff envelopes)
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
|
-
import {
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
19
20
|
import { tmpdir } from "node:os";
|
|
20
21
|
import { join } from "node:path";
|
|
21
|
-
import { randomUUID } from "node:crypto";
|
|
22
22
|
import { buildTeammatePrompt } from "../adapter.js";
|
|
23
23
|
export const PRESETS = {
|
|
24
24
|
claude: {
|
|
25
25
|
name: "claude",
|
|
26
26
|
command: "claude",
|
|
27
|
-
buildArgs(
|
|
27
|
+
buildArgs(_ctx, _teammate, options) {
|
|
28
28
|
const args = ["-p", "--verbose", "--dangerously-skip-permissions"];
|
|
29
29
|
if (options.model)
|
|
30
30
|
args.push("--model", options.model);
|
|
31
|
-
args.push("--", prompt);
|
|
32
31
|
return args;
|
|
33
32
|
},
|
|
34
33
|
env: { FORCE_COLOR: "1", CLAUDECODE: "" },
|
|
34
|
+
stdinPrompt: true,
|
|
35
35
|
},
|
|
36
36
|
codex: {
|
|
37
37
|
name: "codex",
|
|
38
38
|
command: "codex",
|
|
39
|
-
buildArgs(
|
|
40
|
-
const args = ["exec",
|
|
39
|
+
buildArgs(_ctx, teammate, options) {
|
|
40
|
+
const args = ["exec", "-"];
|
|
41
41
|
if (teammate.cwd)
|
|
42
42
|
args.push("-C", teammate.cwd);
|
|
43
43
|
const sandbox = teammate.sandbox ?? options.defaultSandbox ?? "workspace-write";
|
|
44
44
|
args.push("-s", sandbox);
|
|
45
45
|
args.push("--full-auto");
|
|
46
|
+
args.push("--ephemeral");
|
|
47
|
+
args.push("--json");
|
|
46
48
|
if (options.model)
|
|
47
49
|
args.push("-m", options.model);
|
|
48
50
|
return args;
|
|
49
51
|
},
|
|
50
|
-
env: {
|
|
52
|
+
env: { NO_COLOR: "1" },
|
|
53
|
+
stdinPrompt: true,
|
|
54
|
+
/** Parse JSONL output from codex exec --json, returning only the last agent message */
|
|
55
|
+
parseOutput(raw) {
|
|
56
|
+
let lastMessage = "";
|
|
57
|
+
for (const line of raw.split("\n")) {
|
|
58
|
+
if (!line.trim())
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const event = JSON.parse(line);
|
|
62
|
+
if (event.type === "item.completed" &&
|
|
63
|
+
event.item?.type === "agent_message") {
|
|
64
|
+
lastMessage = event.item.text;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* skip non-JSON lines */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return lastMessage || raw;
|
|
72
|
+
},
|
|
51
73
|
},
|
|
52
74
|
aider: {
|
|
53
75
|
name: "aider",
|
|
54
76
|
command: "aider",
|
|
55
|
-
buildArgs({ promptFile },
|
|
77
|
+
buildArgs({ promptFile }, _teammate, options) {
|
|
56
78
|
const args = ["--message-file", promptFile, "--yes", "--no-git"];
|
|
57
79
|
if (options.model)
|
|
58
80
|
args.push("--model", options.model);
|
|
@@ -89,17 +111,26 @@ export class CliProxyAdapter {
|
|
|
89
111
|
}
|
|
90
112
|
async startSession(teammate) {
|
|
91
113
|
const id = `${this.name}-${teammate.name}-${nextId++}`;
|
|
92
|
-
// Create session file
|
|
114
|
+
// Create session file inside .teammates/.tmp so sandboxed agents can access it
|
|
93
115
|
if (!this.sessionsDir) {
|
|
94
|
-
|
|
116
|
+
const tmpBase = join(teammate.cwd ?? process.cwd(), ".teammates", ".tmp");
|
|
117
|
+
this.sessionsDir = join(tmpBase, "sessions");
|
|
95
118
|
await mkdir(this.sessionsDir, { recursive: true });
|
|
119
|
+
// Ensure .tmp is gitignored
|
|
120
|
+
const gitignorePath = join(tmpBase, "..", ".gitignore");
|
|
121
|
+
const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
|
|
122
|
+
if (!existing.includes(".tmp/")) {
|
|
123
|
+
await writeFile(gitignorePath, existing +
|
|
124
|
+
(existing.endsWith("\n") || !existing ? "" : "\n") +
|
|
125
|
+
".tmp/\n").catch(() => { });
|
|
126
|
+
}
|
|
96
127
|
}
|
|
97
128
|
const sessionFile = join(this.sessionsDir, `${teammate.name}.md`);
|
|
98
129
|
await writeFile(sessionFile, `# Session — ${teammate.name}\n\n`, "utf-8");
|
|
99
130
|
this.sessionFiles.set(teammate.name, sessionFile);
|
|
100
131
|
return id;
|
|
101
132
|
}
|
|
102
|
-
async executeTask(
|
|
133
|
+
async executeTask(_sessionId, teammate, prompt) {
|
|
103
134
|
// If the teammate has no soul (e.g. the raw agent), skip identity/memory
|
|
104
135
|
// wrapping but include handoff instructions so it can delegate to teammates
|
|
105
136
|
const sessionFile = this.sessionFiles.get(teammate.name);
|
|
@@ -124,10 +155,8 @@ export class CliProxyAdapter {
|
|
|
124
155
|
: "";
|
|
125
156
|
parts.push(`- @${t.name}: ${t.role}${owns}`);
|
|
126
157
|
}
|
|
127
|
-
parts.push("\nTo hand off,
|
|
128
|
-
parts.push("```
|
|
129
|
-
parts.push('{ "handoff": { "to": "<teammate>", "task": "<what you need them to do>", "context": "<any context>" } }');
|
|
130
|
-
parts.push("```");
|
|
158
|
+
parts.push("\nTo hand off, include a fenced handoff block in your response:");
|
|
159
|
+
parts.push("```handoff\n@<teammate>\n<task details>\n```");
|
|
131
160
|
}
|
|
132
161
|
fullPrompt = parts.join("\n");
|
|
133
162
|
}
|
|
@@ -136,7 +165,10 @@ export class CliProxyAdapter {
|
|
|
136
165
|
await writeFile(promptFile, fullPrompt, "utf-8");
|
|
137
166
|
this.pendingTempFiles.add(promptFile);
|
|
138
167
|
try {
|
|
139
|
-
const
|
|
168
|
+
const rawOutput = await this.spawnAndProxy(teammate, promptFile, fullPrompt);
|
|
169
|
+
const output = this.preset.parseOutput
|
|
170
|
+
? this.preset.parseOutput(rawOutput)
|
|
171
|
+
: rawOutput;
|
|
140
172
|
const teammateNames = this.roster.map((r) => r.name);
|
|
141
173
|
return parseResult(teammate.name, output, teammateNames, prompt);
|
|
142
174
|
}
|
|
@@ -163,15 +195,39 @@ export class CliProxyAdapter {
|
|
|
163
195
|
await writeFile(promptFile, prompt, "utf-8");
|
|
164
196
|
try {
|
|
165
197
|
const command = this.options.commandPath ?? this.preset.command;
|
|
166
|
-
const args = this.preset.buildArgs({ promptFile, prompt }, {
|
|
198
|
+
const args = this.preset.buildArgs({ promptFile, prompt }, {
|
|
199
|
+
name: "_router",
|
|
200
|
+
role: "",
|
|
201
|
+
soul: "",
|
|
202
|
+
wisdom: "",
|
|
203
|
+
dailyLogs: [],
|
|
204
|
+
weeklyLogs: [],
|
|
205
|
+
ownership: { primary: [], secondary: [] },
|
|
206
|
+
routingKeywords: [],
|
|
207
|
+
}, { ...this.options, model: this.options.model ?? "haiku" });
|
|
167
208
|
const env = { ...process.env, ...this.preset.env };
|
|
209
|
+
// Suppress Node.js ExperimentalWarning in routing subprocesses
|
|
210
|
+
const existingNodeOpts = env.NODE_OPTIONS ?? "";
|
|
211
|
+
if (!existingNodeOpts.includes("--disable-warning=ExperimentalWarning")) {
|
|
212
|
+
env.NODE_OPTIONS = existingNodeOpts
|
|
213
|
+
? `${existingNodeOpts} --disable-warning=ExperimentalWarning`
|
|
214
|
+
: "--disable-warning=ExperimentalWarning";
|
|
215
|
+
}
|
|
168
216
|
const output = await new Promise((resolve, reject) => {
|
|
169
|
-
const
|
|
217
|
+
const routeStdin = this.preset.stdinPrompt ?? false;
|
|
218
|
+
const needsShell = this.preset.shell ?? process.platform === "win32";
|
|
219
|
+
const spawnCmd = needsShell ? [command, ...args].join(" ") : command;
|
|
220
|
+
const spawnArgs = needsShell ? [] : args;
|
|
221
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
170
222
|
cwd: process.cwd(),
|
|
171
223
|
env,
|
|
172
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
173
|
-
shell:
|
|
224
|
+
stdio: [routeStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
225
|
+
shell: needsShell,
|
|
174
226
|
});
|
|
227
|
+
if (routeStdin && child.stdin) {
|
|
228
|
+
child.stdin.write(prompt);
|
|
229
|
+
child.stdin.end();
|
|
230
|
+
}
|
|
175
231
|
const captured = [];
|
|
176
232
|
child.stdout?.on("data", (chunk) => captured.push(chunk));
|
|
177
233
|
child.stderr?.on("data", (chunk) => captured.push(chunk));
|
|
@@ -193,7 +249,8 @@ export class CliProxyAdapter {
|
|
|
193
249
|
const trimmed = output.trim().toLowerCase();
|
|
194
250
|
// Check each name — the agent should have returned just one
|
|
195
251
|
for (const name of rosterNames) {
|
|
196
|
-
if (trimmed === name.toLowerCase() ||
|
|
252
|
+
if (trimmed === name.toLowerCase() ||
|
|
253
|
+
trimmed.endsWith(name.toLowerCase())) {
|
|
197
254
|
return name;
|
|
198
255
|
}
|
|
199
256
|
}
|
|
@@ -212,7 +269,7 @@ export class CliProxyAdapter {
|
|
|
212
269
|
await unlink(promptFile).catch(() => { });
|
|
213
270
|
}
|
|
214
271
|
}
|
|
215
|
-
async destroySession(
|
|
272
|
+
async destroySession(_sessionId) {
|
|
216
273
|
// Clean up any leaked temp prompt files
|
|
217
274
|
for (const file of this.pendingTempFiles) {
|
|
218
275
|
await unlink(file).catch(() => { });
|
|
@@ -237,12 +294,32 @@ export class CliProxyAdapter {
|
|
|
237
294
|
const env = { ...process.env, ...this.preset.env };
|
|
238
295
|
const timeout = this.options.timeout ?? 600_000;
|
|
239
296
|
const interactive = this.preset.interactive ?? false;
|
|
240
|
-
const
|
|
297
|
+
const useStdin = this.preset.stdinPrompt ?? false;
|
|
298
|
+
// Suppress Node.js ExperimentalWarning (e.g. SQLite) in agent
|
|
299
|
+
// subprocesses so it doesn't leak into the terminal UI.
|
|
300
|
+
const existingNodeOpts = env.NODE_OPTIONS ?? "";
|
|
301
|
+
if (!existingNodeOpts.includes("--disable-warning=ExperimentalWarning")) {
|
|
302
|
+
env.NODE_OPTIONS = existingNodeOpts
|
|
303
|
+
? `${existingNodeOpts} --disable-warning=ExperimentalWarning`
|
|
304
|
+
: "--disable-warning=ExperimentalWarning";
|
|
305
|
+
}
|
|
306
|
+
// On Windows, npm-installed CLIs are .cmd wrappers that require shell.
|
|
307
|
+
// When using shell mode, pass command+args as a single string to avoid
|
|
308
|
+
// Node DEP0190 deprecation warning about unescaped args with shell: true.
|
|
309
|
+
const needsShell = this.preset.shell ?? process.platform === "win32";
|
|
310
|
+
const spawnCmd = needsShell ? [command, ...args].join(" ") : command;
|
|
311
|
+
const spawnArgs = needsShell ? [] : args;
|
|
312
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
241
313
|
cwd: teammate.cwd ?? process.cwd(),
|
|
242
314
|
env,
|
|
243
|
-
stdio: [interactive ? "pipe" : "ignore", "pipe", "pipe"],
|
|
244
|
-
shell:
|
|
315
|
+
stdio: [interactive || useStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
316
|
+
shell: needsShell,
|
|
245
317
|
});
|
|
318
|
+
// Pipe prompt via stdin if the preset requires it
|
|
319
|
+
if (useStdin && child.stdin) {
|
|
320
|
+
child.stdin.write(fullPrompt);
|
|
321
|
+
child.stdin.end();
|
|
322
|
+
}
|
|
246
323
|
// ── Timeout with SIGTERM → SIGKILL escalation ──────────────
|
|
247
324
|
let killed = false;
|
|
248
325
|
let killTimer = null;
|
|
@@ -260,7 +337,7 @@ export class CliProxyAdapter {
|
|
|
260
337
|
}, timeout);
|
|
261
338
|
// Connect user's stdin → child only if agent may ask questions
|
|
262
339
|
let onUserInput = null;
|
|
263
|
-
if (interactive && child.stdin) {
|
|
340
|
+
if (interactive && !useStdin && child.stdin) {
|
|
264
341
|
onUserInput = (chunk) => {
|
|
265
342
|
child.stdin?.write(chunk);
|
|
266
343
|
};
|
|
@@ -285,11 +362,11 @@ export class CliProxyAdapter {
|
|
|
285
362
|
process.stdin.removeListener("data", onUserInput);
|
|
286
363
|
}
|
|
287
364
|
};
|
|
288
|
-
child.on("close", (
|
|
365
|
+
child.on("close", (_code) => {
|
|
289
366
|
cleanup();
|
|
290
367
|
const output = Buffer.concat(captured).toString("utf-8");
|
|
291
368
|
if (killed) {
|
|
292
|
-
resolve(output
|
|
369
|
+
resolve(`${output}\n\n[TIMEOUT] Agent process killed after ${timeout}ms`);
|
|
293
370
|
}
|
|
294
371
|
else {
|
|
295
372
|
resolve(output);
|
|
@@ -303,66 +380,90 @@ export class CliProxyAdapter {
|
|
|
303
380
|
}
|
|
304
381
|
}
|
|
305
382
|
// ─── Output parsing (shared across all agents) ─────────────────────
|
|
306
|
-
function parseResult(teammateName, output, teammateNames = [],
|
|
307
|
-
//
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
// Fallback: scrape what we can from freeform output
|
|
383
|
+
export function parseResult(teammateName, output, teammateNames = [], _originalTask) {
|
|
384
|
+
// Parse the TO: / # Subject protocol
|
|
385
|
+
const parsed = parseMessageProtocol(output, teammateName, teammateNames);
|
|
386
|
+
if (parsed)
|
|
387
|
+
return parsed;
|
|
388
|
+
// Fallback: no structured output detected
|
|
313
389
|
return {
|
|
314
390
|
teammate: teammateName,
|
|
315
391
|
success: true,
|
|
316
|
-
summary:
|
|
392
|
+
summary: "",
|
|
317
393
|
changedFiles: parseChangedFiles(output),
|
|
318
|
-
|
|
394
|
+
handoffs: [],
|
|
319
395
|
rawOutput: output,
|
|
320
396
|
};
|
|
321
397
|
}
|
|
322
398
|
/**
|
|
323
|
-
* Parse the
|
|
324
|
-
*
|
|
399
|
+
* Parse the message protocol from agent output.
|
|
400
|
+
*
|
|
401
|
+
* Detects two things:
|
|
402
|
+
* 1. ```handoff blocks — fenced code blocks with language "handoff"
|
|
403
|
+
* containing @<teammate> on the first line and the task body below.
|
|
404
|
+
* 2. TO: / # Subject headers for message framing.
|
|
405
|
+
*
|
|
406
|
+
* The ```handoff block is the primary handoff signal and works reliably
|
|
407
|
+
* regardless of where it appears in the output.
|
|
325
408
|
*/
|
|
326
|
-
function
|
|
327
|
-
const
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
if (parsed.handoff && parsed.handoff.to && parsed.handoff.task) {
|
|
341
|
-
const h = parsed.handoff;
|
|
342
|
-
return {
|
|
343
|
-
success: true,
|
|
344
|
-
summary: h.context ?? h.task,
|
|
345
|
-
changedFiles: h.changedFiles ?? h.changed_files ?? [],
|
|
346
|
-
handoff: {
|
|
347
|
-
from: h.from ?? "",
|
|
348
|
-
to: h.to,
|
|
349
|
-
task: h.task,
|
|
350
|
-
changedFiles: h.changedFiles ?? h.changed_files,
|
|
351
|
-
acceptanceCriteria: h.acceptanceCriteria ?? h.acceptance_criteria,
|
|
352
|
-
openQuestions: h.openQuestions ?? h.open_questions,
|
|
353
|
-
context: h.context,
|
|
354
|
-
},
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
// Not valid JSON
|
|
409
|
+
function parseMessageProtocol(output, teammateName, _teammateNames) {
|
|
410
|
+
const lines = output.split("\n");
|
|
411
|
+
// Find # Subject heading
|
|
412
|
+
let subjectLineIdx = -1;
|
|
413
|
+
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
414
|
+
// Skip TO: lines
|
|
415
|
+
if (lines[i].match(/^TO:\s/i))
|
|
416
|
+
continue;
|
|
417
|
+
const headingMatch = lines[i].match(/^#\s+(.+)/);
|
|
418
|
+
if (headingMatch) {
|
|
419
|
+
subjectLineIdx = i;
|
|
420
|
+
break;
|
|
360
421
|
}
|
|
361
422
|
}
|
|
362
|
-
|
|
423
|
+
// Find all ```handoff blocks
|
|
424
|
+
const handoffBlocks = findHandoffBlocks(output);
|
|
425
|
+
const handoffs = handoffBlocks.map((h) => ({
|
|
426
|
+
from: teammateName,
|
|
427
|
+
to: h.target,
|
|
428
|
+
task: h.task,
|
|
429
|
+
}));
|
|
430
|
+
// If no heading and no handoffs, can't parse
|
|
431
|
+
if (subjectLineIdx < 0 && handoffs.length === 0)
|
|
432
|
+
return null;
|
|
433
|
+
const subject = subjectLineIdx >= 0
|
|
434
|
+
? lines[subjectLineIdx].replace(/^#\s+/, "").trim()
|
|
435
|
+
: "";
|
|
436
|
+
return {
|
|
437
|
+
teammate: teammateName,
|
|
438
|
+
success: true,
|
|
439
|
+
summary: subject,
|
|
440
|
+
changedFiles: parseChangedFiles(output),
|
|
441
|
+
handoffs,
|
|
442
|
+
rawOutput: output,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Find a ```handoff fenced code block in the output.
|
|
447
|
+
*
|
|
448
|
+
* Format:
|
|
449
|
+
* ```handoff
|
|
450
|
+
* @<teammate>
|
|
451
|
+
* <task description>
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
454
|
+
* Returns the target teammate name and task body, or null.
|
|
455
|
+
*/
|
|
456
|
+
function findHandoffBlocks(output) {
|
|
457
|
+
const results = [];
|
|
458
|
+
const pattern = /```handoff\s*\n@(\w+)\s*\n([\s\S]*?)```/gi;
|
|
459
|
+
let match;
|
|
460
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
461
|
+
results.push({ target: match[1].toLowerCase(), task: match[2].trim() });
|
|
462
|
+
}
|
|
463
|
+
return results;
|
|
363
464
|
}
|
|
364
465
|
/** Extract file paths from agent output. */
|
|
365
|
-
function parseChangedFiles(output) {
|
|
466
|
+
export function parseChangedFiles(output) {
|
|
366
467
|
const files = new Set();
|
|
367
468
|
// diff --git a/path b/path
|
|
368
469
|
for (const match of output.matchAll(/diff --git a\/(.+?) b\//g)) {
|
|
@@ -374,90 +475,3 @@ function parseChangedFiles(output) {
|
|
|
374
475
|
}
|
|
375
476
|
return Array.from(files);
|
|
376
477
|
}
|
|
377
|
-
/**
|
|
378
|
-
* Look for a JSON handoff envelope in the output.
|
|
379
|
-
* Agents request handoffs by including a fenced JSON block:
|
|
380
|
-
*
|
|
381
|
-
* ```json
|
|
382
|
-
* { "handoff": { "to": "tester", "task": "...", ... } }
|
|
383
|
-
* ```
|
|
384
|
-
*/
|
|
385
|
-
function parseHandoffEnvelope(output) {
|
|
386
|
-
const jsonBlocks = output.matchAll(/```json\s*\n([\s\S]*?)```/g);
|
|
387
|
-
for (const match of jsonBlocks) {
|
|
388
|
-
const block = match[1].trim();
|
|
389
|
-
if (!block.includes('"handoff"') && !block.includes('"to"'))
|
|
390
|
-
continue;
|
|
391
|
-
try {
|
|
392
|
-
const parsed = JSON.parse(block);
|
|
393
|
-
const envelope = parsed.handoff ?? parsed;
|
|
394
|
-
if (envelope.to && envelope.task) {
|
|
395
|
-
return {
|
|
396
|
-
from: envelope.from ?? "",
|
|
397
|
-
to: envelope.to,
|
|
398
|
-
task: envelope.task,
|
|
399
|
-
changedFiles: envelope.changedFiles ?? envelope.changed_files,
|
|
400
|
-
acceptanceCriteria: envelope.acceptanceCriteria ?? envelope.acceptance_criteria,
|
|
401
|
-
openQuestions: envelope.openQuestions ?? envelope.open_questions,
|
|
402
|
-
context: envelope.context,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
// Not valid JSON, skip
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Detect handoff intent from plain-text @mentions in the agent's response.
|
|
414
|
-
* Catches cases like "This is in @beacon's domain. Let me hand it off."
|
|
415
|
-
* where the agent didn't produce the structured JSON block.
|
|
416
|
-
*/
|
|
417
|
-
/**
|
|
418
|
-
* Detect handoff intent from plain-text @mentions in the agent's response.
|
|
419
|
-
* Catches cases like "This is in @beacon's domain. Let me hand it off."
|
|
420
|
-
* where the agent didn't produce the structured JSON block.
|
|
421
|
-
*/
|
|
422
|
-
function parseHandoffFromMention(fromTeammate, output, teammateNames, originalTask) {
|
|
423
|
-
if (teammateNames.length === 0)
|
|
424
|
-
return null;
|
|
425
|
-
// Look for @teammate mentions (excluding the agent's own name)
|
|
426
|
-
const others = teammateNames.filter((n) => n !== fromTeammate);
|
|
427
|
-
if (others.length === 0)
|
|
428
|
-
return null;
|
|
429
|
-
// Match @name patterns — require handoff-like language nearby
|
|
430
|
-
const handoffPatterns = /\bhand(?:ing)?\s*(?:it\s+)?off\b|\bdelegate\b|\broute\b|\bpass(?:ing)?\s+(?:it\s+)?(?:to|along)\b|\bbelong(?:s)?\s+to\b|\b(?:is\s+in)\s+@\w+'?s?\s+domain\b/i;
|
|
431
|
-
for (const name of others) {
|
|
432
|
-
const mentionPattern = new RegExp(`@${name}\\b`, "i");
|
|
433
|
-
if (mentionPattern.test(output) && handoffPatterns.test(output)) {
|
|
434
|
-
return {
|
|
435
|
-
from: fromTeammate,
|
|
436
|
-
to: name,
|
|
437
|
-
task: originalTask || output.slice(0, 200).trim(),
|
|
438
|
-
context: output.replace(/```json[\s\S]*?```/g, "").trim().slice(0, 500),
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
/** Extract a summary from agent output. */
|
|
445
|
-
function extractSummary(output) {
|
|
446
|
-
// Look for a "## Summary" or "Summary:" section
|
|
447
|
-
const summaryMatch = output.match(/(?:##?\s*Summary|Summary:)\s*\n([\s\S]*?)(?:\n##|\n---|\n```|$)/i);
|
|
448
|
-
if (summaryMatch) {
|
|
449
|
-
const summary = summaryMatch[1].trim();
|
|
450
|
-
if (summary.length > 0)
|
|
451
|
-
return summary.slice(0, 500);
|
|
452
|
-
}
|
|
453
|
-
// Fall back to last non-empty paragraph
|
|
454
|
-
const paragraphs = output
|
|
455
|
-
.split(/\n\s*\n/)
|
|
456
|
-
.map((p) => p.trim())
|
|
457
|
-
.filter((p) => p.length > 0 && !p.startsWith("```"));
|
|
458
|
-
if (paragraphs.length > 0) {
|
|
459
|
-
const last = paragraphs[paragraphs.length - 1];
|
|
460
|
-
return last.length > 500 ? last.slice(0, 497) + "..." : last;
|
|
461
|
-
}
|
|
462
|
-
return output.slice(0, 200).trim();
|
|
463
|
-
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot SDK adapter — uses the @github/copilot-sdk to run tasks
|
|
3
|
+
* through GitHub Copilot's agentic coding engine.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the CLI proxy adapter (which spawns subprocesses), this adapter
|
|
6
|
+
* communicates with Copilot via the SDK's JSON-RPC protocol, giving us:
|
|
7
|
+
* - Structured event streaming (no stdout scraping)
|
|
8
|
+
* - Session persistence via Copilot's infinite sessions
|
|
9
|
+
* - Direct tool/permission control
|
|
10
|
+
* - Access to Copilot's built-in coding tools (file ops, git, bash, etc.)
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentAdapter, InstalledService, RosterEntry } from "../adapter.js";
|
|
13
|
+
import type { TaskResult, TeammateConfig } from "../types.js";
|
|
14
|
+
export interface CopilotAdapterOptions {
|
|
15
|
+
/** Model override (e.g. "gpt-4o", "claude-sonnet-4-5") */
|
|
16
|
+
model?: string;
|
|
17
|
+
/** Timeout in ms for sendAndWait (default: 600_000 = 10 min) */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/** GitHub token for authentication (falls back to env/logged-in user) */
|
|
20
|
+
githubToken?: string;
|
|
21
|
+
/** Custom provider config for BYOK mode */
|
|
22
|
+
provider?: {
|
|
23
|
+
type?: "openai" | "azure" | "anthropic";
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare class CopilotAdapter implements AgentAdapter {
|
|
29
|
+
readonly name = "copilot";
|
|
30
|
+
/** Team roster — set by the orchestrator so prompts include teammate info. */
|
|
31
|
+
roster: RosterEntry[];
|
|
32
|
+
/** Installed services — set by the CLI so prompts include service info. */
|
|
33
|
+
services: InstalledService[];
|
|
34
|
+
private options;
|
|
35
|
+
private client;
|
|
36
|
+
private sessions;
|
|
37
|
+
/** Session files per teammate — persists state across task invocations. */
|
|
38
|
+
private sessionFiles;
|
|
39
|
+
/** Base directory for session files. */
|
|
40
|
+
private sessionsDir;
|
|
41
|
+
constructor(options?: CopilotAdapterOptions);
|
|
42
|
+
startSession(teammate: TeammateConfig): Promise<string>;
|
|
43
|
+
executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
|
|
44
|
+
routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
|
|
45
|
+
destroySession(_sessionId: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Ensure the CopilotClient is started.
|
|
48
|
+
*/
|
|
49
|
+
private ensureClient;
|
|
50
|
+
}
|