@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.
Files changed (76) hide show
  1. package/README.md +31 -22
  2. package/dist/adapter.d.ts +1 -1
  3. package/dist/adapter.js +68 -56
  4. package/dist/adapter.test.js +34 -21
  5. package/dist/adapters/cli-proxy.d.ts +11 -4
  6. package/dist/adapters/cli-proxy.js +176 -162
  7. package/dist/adapters/copilot.d.ts +50 -0
  8. package/dist/adapters/copilot.js +210 -0
  9. package/dist/adapters/echo.d.ts +2 -2
  10. package/dist/adapters/echo.js +2 -1
  11. package/dist/adapters/echo.test.js +4 -2
  12. package/dist/cli-utils.d.ts +21 -0
  13. package/dist/cli-utils.js +74 -0
  14. package/dist/cli-utils.test.d.ts +1 -0
  15. package/dist/cli-utils.test.js +179 -0
  16. package/dist/cli.js +3160 -961
  17. package/dist/compact.d.ts +39 -0
  18. package/dist/compact.js +269 -0
  19. package/dist/compact.test.d.ts +1 -0
  20. package/dist/compact.test.js +198 -0
  21. package/dist/console/ansi.d.ts +18 -0
  22. package/dist/console/ansi.js +20 -0
  23. package/dist/console/ansi.test.d.ts +1 -0
  24. package/dist/console/ansi.test.js +50 -0
  25. package/dist/console/dropdown.d.ts +23 -0
  26. package/dist/console/dropdown.js +63 -0
  27. package/dist/console/file-drop.d.ts +59 -0
  28. package/dist/console/file-drop.js +186 -0
  29. package/dist/console/file-drop.test.d.ts +1 -0
  30. package/dist/console/file-drop.test.js +145 -0
  31. package/dist/console/index.d.ts +22 -0
  32. package/dist/console/index.js +23 -0
  33. package/dist/console/interactive-readline.d.ts +65 -0
  34. package/dist/console/interactive-readline.js +132 -0
  35. package/dist/console/markdown-table.d.ts +17 -0
  36. package/dist/console/markdown-table.js +270 -0
  37. package/dist/console/markdown-table.test.d.ts +1 -0
  38. package/dist/console/markdown-table.test.js +130 -0
  39. package/dist/console/mutable-output.d.ts +21 -0
  40. package/dist/console/mutable-output.js +51 -0
  41. package/dist/console/paste-handler.d.ts +63 -0
  42. package/dist/console/paste-handler.js +177 -0
  43. package/dist/console/prompt-box.d.ts +55 -0
  44. package/dist/console/prompt-box.js +120 -0
  45. package/dist/console/prompt-input.d.ts +136 -0
  46. package/dist/console/prompt-input.js +618 -0
  47. package/dist/console/startup.d.ts +20 -0
  48. package/dist/console/startup.js +138 -0
  49. package/dist/console/startup.test.d.ts +1 -0
  50. package/dist/console/startup.test.js +41 -0
  51. package/dist/console/wordwheel.d.ts +75 -0
  52. package/dist/console/wordwheel.js +123 -0
  53. package/dist/dropdown.js +4 -21
  54. package/dist/index.d.ts +5 -5
  55. package/dist/index.js +3 -3
  56. package/dist/onboard.d.ts +24 -0
  57. package/dist/onboard.js +174 -11
  58. package/dist/orchestrator.d.ts +8 -11
  59. package/dist/orchestrator.js +33 -81
  60. package/dist/orchestrator.test.js +59 -79
  61. package/dist/registry.d.ts +1 -1
  62. package/dist/registry.js +56 -12
  63. package/dist/registry.test.js +57 -13
  64. package/dist/theme.d.ts +56 -0
  65. package/dist/theme.js +54 -0
  66. package/dist/types.d.ts +18 -13
  67. package/package.json +8 -3
  68. package/template/CROSS-TEAM.md +2 -2
  69. package/template/PROTOCOL.md +72 -15
  70. package/template/README.md +2 -2
  71. package/template/TEMPLATE.md +118 -15
  72. package/template/example/SOUL.md +2 -1
  73. package/template/example/WISDOM.md +9 -0
  74. package/dist/adapters/codex.d.ts +0 -50
  75. package/dist/adapters/codex.js +0 -213
  76. 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 { writeFile, unlink, mkdir } from "node:fs/promises";
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({ prompt }, teammate, options) {
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({ prompt }, teammate, options) {
40
- const args = ["exec", prompt];
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: { FORCE_COLOR: "1" },
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 }, teammate, options) {
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 for this teammate
114
+ // Create session file inside .teammates/.tmp so sandboxed agents can access it
93
115
  if (!this.sessionsDir) {
94
- this.sessionsDir = join(tmpdir(), `teammates-sessions-${randomUUID()}`);
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(sessionId, teammate, prompt) {
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, end your response with:");
128
- parts.push("```json");
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 output = await this.spawnAndProxy(teammate, promptFile, fullPrompt);
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 }, { name: "_router", role: "", soul: "", memories: "", dailyLogs: [], ownership: { primary: [], secondary: [] } }, { ...this.options, model: this.options.model ?? "haiku" });
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 child = spawn(command, args, {
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: this.preset.shell ?? false,
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() || trimmed.endsWith(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(sessionId) {
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 child = spawn(command, args, {
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: this.preset.shell ?? false,
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", (code) => {
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 + `\n\n[TIMEOUT] Agent process killed after ${timeout}ms`);
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 = [], originalTask) {
307
- // Try to parse the structured JSON block the agent was asked to produce
308
- const structured = parseStructuredOutput(output);
309
- if (structured) {
310
- return { ...structured, teammate: teammateName, rawOutput: output };
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: extractSummary(output),
392
+ summary: "",
317
393
  changedFiles: parseChangedFiles(output),
318
- handoff: parseHandoffEnvelope(output) ?? parseHandoffFromMention(teammateName, output, teammateNames, originalTask) ?? undefined,
394
+ handoffs: [],
319
395
  rawOutput: output,
320
396
  };
321
397
  }
322
398
  /**
323
- * Parse the structured JSON block from the output protocol.
324
- * Looks for either { "result": ... } or { "handoff": ... }.
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 parseStructuredOutput(output) {
327
- const jsonBlocks = [...output.matchAll(/```json\s*\n([\s\S]*?)```/g)];
328
- // Take the last JSON block — that's the protocol output
329
- for (let i = jsonBlocks.length - 1; i >= 0; i--) {
330
- const block = jsonBlocks[i][1].trim();
331
- try {
332
- const parsed = JSON.parse(block);
333
- if (parsed.result) {
334
- return {
335
- success: true,
336
- summary: parsed.result.summary ?? "",
337
- changedFiles: parsed.result.changedFiles ?? parsed.result.changed_files ?? [],
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
- return null;
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
+ }