@sdods/agents 0.19.0 → 0.21.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.
@@ -0,0 +1,45 @@
1
+ import { type CliAdapterContext } from './cli-common.js';
2
+ import { type AgentEvent, type CompleteRequest, type CompleteResult, type LlmAdapter, type RunAgentOptions, type RunAgentResult, type TokenUsage } from './types.js';
3
+ /**
4
+ * Copilot tool permissions the agent never gets, whatever `--allow-all-tools` says (denials win).
5
+ * `shell` and `write` are Copilot's own kinds for commands and file edits; `url` covers its web
6
+ * fetches. The review-only sdods tools stay with a person.
7
+ */
8
+ export declare const COPILOT_DENIED_TOOLS: string[];
9
+ /**
10
+ * Copilot `--output-format json` events → SDODS events. Every payload sits under `data`
11
+ * (schema: `copilot-sdk/generated/session-events.d.ts`, Copilot CLI 1.0.105).
12
+ */
13
+ export declare function normalizeCopilotEvent(ev: Record<string, any>): AgentEvent[];
14
+ /** Sums the per-call `assistant.usage` events into one total. */
15
+ export declare function addCopilotUsage(total: TokenUsage | undefined, d: Record<string, any>): TokenUsage;
16
+ /**
17
+ * Runs SDODS agent roles through the user's logged-in GitHub Copilot CLI (`copilot -p
18
+ * --output-format json`). No API key: the Copilot subscription (or COPILOT_GITHUB_TOKEN / GH_TOKEN)
19
+ * pays. The sdods MCP server goes in with `--additional-mcp-config`; the built-in GitHub MCP
20
+ * servers are switched off, and shell, file writes and web fetches are denied.
21
+ *
22
+ * Copilot has no per-path read rule like Claude Code's `Read(**\/.env)`. Its file access is held to
23
+ * the working directory, so point `cwd` at the project and keep secrets out of it (`${VAR}`).
24
+ */
25
+ export declare class CopilotCliAdapter implements LlmAdapter {
26
+ private readonly ctx;
27
+ readonly provider: "copilot";
28
+ readonly defaultModel: string;
29
+ constructor(ctx?: CliAdapterContext & {
30
+ model?: string;
31
+ });
32
+ /** Copilot has no non-interactive login check; a missing login shows up on the first call. */
33
+ bin(): string;
34
+ static loginStatus(bin?: string): {
35
+ installed: boolean;
36
+ loggedIn: boolean | null;
37
+ detail: string;
38
+ };
39
+ private baseArgs;
40
+ private denyArgs;
41
+ complete(req: CompleteRequest): Promise<CompleteResult>;
42
+ stream(req: CompleteRequest): AsyncIterable<string>;
43
+ runAgent(o: RunAgentOptions): Promise<RunAgentResult>;
44
+ }
45
+ //# sourceMappingURL=copilot.d.ts.map
@@ -0,0 +1,257 @@
1
+ import { mcpServerMap, requireCli, removeTempJson, runJsonl, runQuiet, writeTempJson, REVIEW_ONLY_TOOLS, } from './cli-common.js';
2
+ import { cliFailure } from './claude-code.js';
3
+ import { DEFAULT_MODELS, } from './types.js';
4
+ const INSTALL_HINT = 'Install GitHub Copilot CLI (`npm i -g @github/copilot`) and run `copilot login`, or use --adapter claude-code, --adapter codex, or --dry-run.';
5
+ /**
6
+ * Copilot tool permissions the agent never gets, whatever `--allow-all-tools` says (denials win).
7
+ * `shell` and `write` are Copilot's own kinds for commands and file edits; `url` covers its web
8
+ * fetches. The review-only sdods tools stay with a person.
9
+ */
10
+ export const COPILOT_DENIED_TOOLS = [
11
+ 'shell',
12
+ 'write',
13
+ 'url',
14
+ ...REVIEW_ONLY_TOOLS.map((t) => `sdods(${t})`),
15
+ ];
16
+ /**
17
+ * Copilot `--output-format json` events → SDODS events. Every payload sits under `data`
18
+ * (schema: `copilot-sdk/generated/session-events.d.ts`, Copilot CLI 1.0.105).
19
+ */
20
+ export function normalizeCopilotEvent(ev) {
21
+ const d = (ev.data ?? {});
22
+ switch (ev.type) {
23
+ case 'assistant.message':
24
+ // sub-agent chatter carries parentToolCallId; keep the transcript to the main agent
25
+ if (d.parentToolCallId || typeof d.content !== 'string' || !d.content)
26
+ return [];
27
+ return [{ type: 'text', text: d.content }];
28
+ case 'tool.execution_start':
29
+ return [
30
+ {
31
+ type: 'tool_call',
32
+ id: String(d.toolCallId),
33
+ name: d.mcpServerName && d.mcpToolName
34
+ ? `mcp__${d.mcpServerName}__${d.mcpToolName}`
35
+ : String(d.toolName ?? 'tool'),
36
+ input: d.arguments ?? null,
37
+ },
38
+ ];
39
+ case 'tool.execution_complete':
40
+ return [
41
+ {
42
+ type: 'tool_result',
43
+ id: String(d.toolCallId),
44
+ output: d.result?.content ?? d.error?.message ?? null,
45
+ isError: d.success === false,
46
+ },
47
+ ];
48
+ case 'session.error':
49
+ return [{ type: 'status', message: String(d.message ?? d.errorType ?? 'error') }];
50
+ case 'session.warning':
51
+ return d.message ? [{ type: 'status', message: String(d.message) }] : [];
52
+ default:
53
+ return [];
54
+ }
55
+ }
56
+ /** Sums the per-call `assistant.usage` events into one total. */
57
+ export function addCopilotUsage(total, d) {
58
+ const add = (a, b) => (typeof b === 'number' ? (a ?? 0) + b : a);
59
+ return {
60
+ inputTokens: add(total?.inputTokens, d.inputTokens),
61
+ outputTokens: add(total?.outputTokens, d.outputTokens),
62
+ cacheReadTokens: add(total?.cacheReadTokens, d.cacheReadTokens),
63
+ cacheWriteTokens: add(total?.cacheWriteTokens, d.cacheWriteTokens),
64
+ };
65
+ }
66
+ /**
67
+ * Runs SDODS agent roles through the user's logged-in GitHub Copilot CLI (`copilot -p
68
+ * --output-format json`). No API key: the Copilot subscription (or COPILOT_GITHUB_TOKEN / GH_TOKEN)
69
+ * pays. The sdods MCP server goes in with `--additional-mcp-config`; the built-in GitHub MCP
70
+ * servers are switched off, and shell, file writes and web fetches are denied.
71
+ *
72
+ * Copilot has no per-path read rule like Claude Code's `Read(**\/.env)`. Its file access is held to
73
+ * the working directory, so point `cwd` at the project and keep secrets out of it (`${VAR}`).
74
+ */
75
+ export class CopilotCliAdapter {
76
+ ctx;
77
+ provider = 'copilot';
78
+ defaultModel;
79
+ constructor(ctx = {}) {
80
+ this.ctx = ctx;
81
+ this.defaultModel = ctx.model ?? process.env.SDODS_COPILOT_MODEL ?? DEFAULT_MODELS.copilot;
82
+ }
83
+ /** Copilot has no non-interactive login check; a missing login shows up on the first call. */
84
+ bin() {
85
+ return requireCli(this.ctx.bin ?? 'copilot', INSTALL_HINT);
86
+ }
87
+ static loginStatus(bin = 'copilot') {
88
+ const found = (() => {
89
+ try {
90
+ return requireCli(bin, '');
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ })();
96
+ if (!found)
97
+ return { installed: false, loggedIn: null, detail: 'not installed' };
98
+ const v = runQuiet(found, ['--version']);
99
+ const token = Boolean(process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
100
+ return {
101
+ installed: true,
102
+ loggedIn: token ? true : null,
103
+ detail: `${v.stdout.trim().split('\n')[0] || 'installed'} · ${token ? 'token in environment' : 'login not verified (run `copilot login` once)'}`,
104
+ };
105
+ }
106
+ baseArgs(o) {
107
+ const m = o.model ?? this.defaultModel;
108
+ return [
109
+ '--output-format',
110
+ 'json',
111
+ '--no-ask-user',
112
+ '--no-auto-update',
113
+ '--no-color',
114
+ '--disable-builtin-mcps',
115
+ ...(o.cwd ? ['-C', o.cwd] : []),
116
+ ...(m && m !== 'default' ? ['--model', m] : []),
117
+ ...(o.effort ? ['--reasoning-effort', o.effort] : []),
118
+ ];
119
+ }
120
+ denyArgs() {
121
+ return COPILOT_DENIED_TOOLS.flatMap((t) => ['--deny-tool', t]);
122
+ }
123
+ async complete(req) {
124
+ const bin = this.bin();
125
+ const prompt = req.system ? `${req.system}\n\n---\n\n${req.prompt}` : req.prompt;
126
+ let text = '';
127
+ let usage;
128
+ let costUsd;
129
+ let model;
130
+ const r = await runJsonl(bin, [
131
+ '-p',
132
+ prompt,
133
+ ...this.baseArgs({ model: req.model, effort: req.effort }),
134
+ '--available-tools',
135
+ '',
136
+ '--no-custom-instructions',
137
+ ], {
138
+ signal: req.signal,
139
+ onLine: (line) => {
140
+ if (typeof line !== 'object' || !line)
141
+ return;
142
+ const ev = line;
143
+ if (ev.type === 'assistant.usage' && ev.data) {
144
+ usage = addCopilotUsage(usage, ev.data);
145
+ if (typeof ev.data.cost === 'number')
146
+ costUsd = (costUsd ?? 0) + ev.data.cost;
147
+ model = ev.data.model ?? model;
148
+ }
149
+ for (const e of normalizeCopilotEvent(ev))
150
+ if (e.type === 'text')
151
+ text += e.text;
152
+ },
153
+ });
154
+ if (r.exitCode !== 0 && !text)
155
+ throw cliFailure('copilot', r.exitCode, r.stderr);
156
+ return { text: text.trim(), usage, costUsd, model };
157
+ }
158
+ async *stream(req) {
159
+ const r = await this.complete(req);
160
+ yield r.text;
161
+ }
162
+ async runAgent(o) {
163
+ const bin = this.bin();
164
+ const servers = mcpServerMap({ ...this.ctx, rootDir: this.ctx.rootDir ?? o.cwd }, o.mcpServers);
165
+ // Copilot names stdio servers `local`; http stays `http`.
166
+ const copilotServers = Object.fromEntries(Object.entries(servers).map(([name, s]) => [
167
+ name,
168
+ { ...s, type: s.type === 'stdio' ? 'local' : s.type, tools: ['*'] },
169
+ ]));
170
+ const mcpFile = writeTempJson('sdods-copilot-', { mcpServers: copilotServers });
171
+ const args = [
172
+ '-p',
173
+ `${o.system}\n\n---\n\n${o.prompt}`,
174
+ ...this.baseArgs({ cwd: o.cwd, model: o.model }),
175
+ '--additional-mcp-config',
176
+ `@${mcpFile}`,
177
+ '--allow-all-tools',
178
+ ...this.denyArgs(),
179
+ ];
180
+ let text = '';
181
+ let turns = 0;
182
+ let toolCalls = 0;
183
+ let usage;
184
+ let costUsd;
185
+ let exitCode;
186
+ let sessionError = false;
187
+ let stoppedAtMaxTurns = false;
188
+ const controller = new AbortController();
189
+ const onAbort = () => controller.abort();
190
+ o.signal?.addEventListener('abort', onAbort, { once: true });
191
+ const r = await runJsonl(bin, args, {
192
+ cwd: o.cwd,
193
+ signal: controller.signal,
194
+ onLine: (line) => {
195
+ if (typeof line !== 'object' || !line)
196
+ return;
197
+ const ev = line;
198
+ if (ev.type === 'assistant.turn_end') {
199
+ turns++;
200
+ if (o.maxTurns && turns >= o.maxTurns && !stoppedAtMaxTurns) {
201
+ stoppedAtMaxTurns = true;
202
+ o.onEvent?.({ type: 'status', message: `max turns (${o.maxTurns}) reached` });
203
+ controller.abort();
204
+ }
205
+ }
206
+ if (ev.type === 'assistant.usage' && ev.data) {
207
+ usage = addCopilotUsage(usage, ev.data);
208
+ if (typeof ev.data.cost === 'number')
209
+ costUsd = (costUsd ?? 0) + ev.data.cost;
210
+ }
211
+ if (ev.type === 'session.error')
212
+ sessionError = true;
213
+ if (ev.type === 'result' && typeof ev.exitCode === 'number')
214
+ exitCode = ev.exitCode;
215
+ for (const e of normalizeCopilotEvent(ev)) {
216
+ if (e.type === 'tool_call')
217
+ toolCalls++;
218
+ if (e.type === 'text')
219
+ text += text ? `\n\n${e.text}` : e.text;
220
+ o.onEvent?.(e);
221
+ }
222
+ },
223
+ }).finally(() => {
224
+ o.signal?.removeEventListener('abort', onAbort);
225
+ removeTempJson(mcpFile);
226
+ });
227
+ const code = exitCode ?? r.exitCode;
228
+ const isError = !stoppedAtMaxTurns && (sessionError || code !== 0);
229
+ if (isError && !text)
230
+ throw cliFailure('copilot', code, r.stderr);
231
+ if (o.maxBudgetUsd && costUsd && costUsd > o.maxBudgetUsd) {
232
+ o.onEvent?.({
233
+ type: 'status',
234
+ message: `budget exceeded: $${costUsd.toFixed(3)} > $${o.maxBudgetUsd}`,
235
+ });
236
+ }
237
+ const result = {
238
+ text,
239
+ usage,
240
+ costUsd,
241
+ turns: Math.max(turns, 1),
242
+ stopReason: stoppedAtMaxTurns ? 'max_turns' : isError ? 'error' : 'end',
243
+ isError,
244
+ toolCalls,
245
+ };
246
+ o.onEvent?.({
247
+ type: 'result',
248
+ usage,
249
+ costUsd,
250
+ turns: result.turns,
251
+ stopReason: result.stopReason,
252
+ isError,
253
+ });
254
+ return result;
255
+ }
256
+ }
257
+ //# sourceMappingURL=copilot.js.map
@@ -20,7 +20,7 @@ export interface CreateAdapterOptions {
20
20
  export declare function normalizeProvider(raw: string): Provider;
21
21
  /**
22
22
  * Detection order when nothing is set explicitly:
23
- * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → OPENAI_API_KEY → Ollama → fake.
23
+ * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → `copilot` CLI → OPENAI_API_KEY → Ollama → fake.
24
24
  *
25
25
  * A key or a CLI login is a deliberate statement of intent, so those come first; an installed
26
26
  * Ollama still beats `fake`, which produces nothing. Detection is synchronous and must not touch
@@ -2,6 +2,7 @@ import { ClaudeAdapter } from './claude.js';
2
2
  import { ClaudeCodeCliAdapter } from './claude-code.js';
3
3
  import { findOnPath } from './cli-common.js';
4
4
  import { CodexCliAdapter } from './codex.js';
5
+ import { CopilotCliAdapter } from './copilot.js';
5
6
  import { FakeAdapter } from './fake.js';
6
7
  import { OllamaAdapter } from './ollama.js';
7
8
  import { OpenAiCompatibleAdapter } from './openai-compat.js';
@@ -14,6 +15,8 @@ export function normalizeProvider(raw) {
14
15
  return 'claude-code';
15
16
  if (p === 'codex' || p === 'codex-cli' || p === 'openai-codex')
16
17
  return 'codex';
18
+ if (p === 'copilot' || p === 'copilot-cli' || p === 'github-copilot' || p === 'gh-copilot')
19
+ return 'copilot';
17
20
  if (p === 'ollama' || p === 'local')
18
21
  return 'ollama';
19
22
  if (p === 'openai' ||
@@ -24,11 +27,11 @@ export function normalizeProvider(raw) {
24
27
  return 'openai-compatible';
25
28
  if (p === 'fake' || p === 'dry-run')
26
29
  return 'fake';
27
- throw new AgentsConfigError(`Unknown LLM provider "${raw}".`, 'Use claude, claude-code, codex, openai-compatible, ollama or fake.');
30
+ throw new AgentsConfigError(`Unknown LLM provider "${raw}".`, 'Use claude, claude-code, codex, copilot, openai-compatible, ollama or fake.');
28
31
  }
29
32
  /**
30
33
  * Detection order when nothing is set explicitly:
31
- * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → OPENAI_API_KEY → Ollama → fake.
34
+ * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → `copilot` CLI → OPENAI_API_KEY → Ollama → fake.
32
35
  *
33
36
  * A key or a CLI login is a deliberate statement of intent, so those come first; an installed
34
37
  * Ollama still beats `fake`, which produces nothing. Detection is synchronous and must not touch
@@ -42,6 +45,8 @@ export function detectProvider(env = process.env) {
42
45
  return { provider: 'claude-code', reason: 'claude CLI found on PATH' };
43
46
  if (findOnPath('codex'))
44
47
  return { provider: 'codex', reason: 'codex CLI found on PATH' };
48
+ if (findOnPath('copilot'))
49
+ return { provider: 'copilot', reason: 'GitHub Copilot CLI found on PATH' };
45
50
  if (env.OPENAI_API_KEY)
46
51
  return { provider: 'openai-compatible', reason: 'OPENAI_API_KEY is set' };
47
52
  if (env.OLLAMA_HOST)
@@ -50,7 +55,7 @@ export function detectProvider(env = process.env) {
50
55
  return { provider: 'ollama', reason: 'ollama found on PATH' };
51
56
  return {
52
57
  provider: 'fake',
53
- reason: 'no API key and no claude/codex CLI found; using the fake adapter',
58
+ reason: 'no API key and no claude/codex/copilot CLI found; using the fake adapter',
54
59
  };
55
60
  }
56
61
  /** Detection plus a short probe of the default Ollama host, for callers that can await. */
@@ -80,6 +85,8 @@ export function createAdapter(opts = {}, projectDefault) {
80
85
  return new ClaudeCodeCliAdapter(cliCtx);
81
86
  case 'codex':
82
87
  return new CodexCliAdapter(cliCtx);
88
+ case 'copilot':
89
+ return new CopilotCliAdapter(cliCtx);
83
90
  case 'openai-compatible':
84
91
  return new OpenAiCompatibleAdapter({
85
92
  model: opts.model,
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './claude.js';
3
3
  export * from './claude-code.js';
4
4
  export * from './codex.js';
5
+ export * from './copilot.js';
5
6
  export * from './cli-common.js';
6
7
  export * from './mcp-bridge.js';
7
8
  export * from './ollama.js';
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './claude.js';
3
3
  export * from './claude-code.js';
4
4
  export * from './codex.js';
5
+ export * from './copilot.js';
5
6
  export * from './cli-common.js';
6
7
  export * from './mcp-bridge.js';
7
8
  export * from './ollama.js';
@@ -1,10 +1,10 @@
1
1
  import type { AgentSdkToolDef } from '@sdods/mcp';
2
2
  /**
3
- * claude / openai-compatible: API keys. claude-code / codex: shell out to the user's logged-in
3
+ * claude / openai-compatible: API keys. claude-code / codex / copilot: shell out to the user's logged-in
4
4
  * CLI (no key needed). ollama: a model running on this machine, no key and no cost.
5
5
  * fake: scripted, for tests and --dry-run.
6
6
  */
7
- export type Provider = 'claude' | 'claude-code' | 'codex' | 'openai-compatible' | 'ollama' | 'fake';
7
+ export type Provider = 'claude' | 'claude-code' | 'codex' | 'copilot' | 'openai-compatible' | 'ollama' | 'fake';
8
8
  export interface TokenUsage {
9
9
  inputTokens?: number;
10
10
  outputTokens?: number;
@@ -104,6 +104,6 @@ export declare class AgentsConfigError extends Error {
104
104
  readonly code = "AGENTS_CONFIG";
105
105
  constructor(message: string, hint?: string | undefined);
106
106
  }
107
- /** `default` means "let the CLI pick its configured model" (claude-code, codex). */
107
+ /** `default` means "let the CLI pick its configured model" (claude-code, codex, copilot). */
108
108
  export declare const DEFAULT_MODELS: Record<Provider, string>;
109
109
  //# sourceMappingURL=types.d.ts.map
@@ -7,11 +7,12 @@ export class AgentsConfigError extends Error {
7
7
  this.name = 'AgentsConfigError';
8
8
  }
9
9
  }
10
- /** `default` means "let the CLI pick its configured model" (claude-code, codex). */
10
+ /** `default` means "let the CLI pick its configured model" (claude-code, codex, copilot). */
11
11
  export const DEFAULT_MODELS = {
12
12
  claude: 'claude-opus-5',
13
13
  'claude-code': 'default',
14
14
  codex: 'default',
15
+ copilot: 'default',
15
16
  'openai-compatible': 'gpt-4.1',
16
17
  // Small enough to run on a laptop, and the best of that size at tool calls and structured output.
17
18
  ollama: 'qwen2.5-coder:7b',
@@ -1,4 +1,5 @@
1
- export type CodingAgentTarget = 'claude' | 'codex' | 'all';
1
+ import { type RoleName } from '@sdods/mcp';
2
+ export type CodingAgentTarget = 'claude' | 'codex' | 'copilot' | 'all';
2
3
  export interface InstallCodingAgentsOptions {
3
4
  for?: CodingAgentTarget;
4
5
  project?: string;
@@ -11,6 +12,8 @@ export interface InstallCodingAgentsOptions {
11
12
  * Set up coding-agent CLIs for an SDODS repo:
12
13
  * - Claude Code: `.claude/agents/sdods-<role>.md` (one subagent per role) + `CLAUDE.md`
13
14
  * - Codex: `AGENTS.md` (Codex reads it as project instructions; roles become sections)
15
+ * - GitHub Copilot: `.github/agents/sdods-<role>.agent.md` (custom agents) + `AGENTS.md`, which
16
+ * Copilot also reads as project instructions
14
17
  * - both: `AGENT.md` and `SKILL.md` with the conventions
15
18
  * Returns the paths written. Existing files are kept unless `force`.
16
19
  */
@@ -26,7 +29,24 @@ export declare function claudeMdContent(opts?: {
26
29
  project?: string;
27
30
  env?: string;
28
31
  }): string;
29
- /** AGENTS.md: Codex reads this as project instructions; roles become sections. */
32
+ /** A Claude Code subagent (`.claude/agents/sdods-<role>.md`) for one SDODS role. */
33
+ export declare function claudeAgentContent(role: RoleName): string;
34
+ /**
35
+ * The generated role agent files and what this version would write in each, so `sdods upgrade`
36
+ * can tell which ones are out of date. Only `sdods-<role>` files count; nothing else is touched.
37
+ */
38
+ export declare function codingAgentFiles(rootDir: string): Array<{
39
+ file: string;
40
+ content: string;
41
+ }>;
42
+ /**
43
+ * A GitHub Copilot custom agent (`.github/agents/<name>.agent.md`), used by Copilot CLI
44
+ * (`copilot --agent sdods-healer`), VS Code and the Copilot coding agent. `read` and `search` are
45
+ * Copilot's file tools; `sdods/*` is every tool on the sdods MCP server. No `edit` or `shell`: the
46
+ * roles change the repo only through proposals.
47
+ */
48
+ export declare function copilotAgentContent(role: RoleName): string;
49
+ /** AGENTS.md: Codex and GitHub Copilot read this as project instructions; roles become sections. */
30
50
  export declare function agentsMdContent(opts?: {
31
51
  project?: string;
32
52
  env?: string;
@@ -17,6 +17,8 @@ function roleUse(role) {
17
17
  * Set up coding-agent CLIs for an SDODS repo:
18
18
  * - Claude Code: `.claude/agents/sdods-<role>.md` (one subagent per role) + `CLAUDE.md`
19
19
  * - Codex: `AGENTS.md` (Codex reads it as project instructions; roles become sections)
20
+ * - GitHub Copilot: `.github/agents/sdods-<role>.agent.md` (custom agents) + `AGENTS.md`, which
21
+ * Copilot also reads as project instructions
20
22
  * - both: `AGENT.md` and `SKILL.md` with the conventions
21
23
  * Returns the paths written. Existing files are kept unless `force`.
22
24
  */
@@ -33,8 +35,7 @@ export function installCodingAgents(rootDir, opts = {}) {
33
35
  if (target === 'claude' || target === 'all') {
34
36
  const agentsDir = join(rootDir, '.claude', 'agents');
35
37
  for (const role of Object.keys(ROLE_PROMPTS)) {
36
- const p = ROLE_PROMPTS[role];
37
- writeIfMissing(join(agentsDir, `sdods-${role}.md`), `---\nname: sdods-${role}\ndescription: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.\ntools: Read, Glob, Grep, mcp__sdods__*\n---\n\n${CONVENTIONS}\n\n${p.body}\n`);
38
+ writeIfMissing(join(agentsDir, `sdods-${role}.md`), claudeAgentContent(role));
38
39
  }
39
40
  writeIfMissing(join(rootDir, 'CLAUDE.md'), claudeMdContent(opts));
40
41
  if (opts.mcp) {
@@ -45,7 +46,13 @@ export function installCodingAgents(rootDir, opts = {}) {
45
46
  written.push(mcp.file);
46
47
  }
47
48
  }
48
- if (target === 'codex' || target === 'all') {
49
+ if (target === 'copilot' || target === 'all') {
50
+ const agentsDir = join(rootDir, '.github', 'agents');
51
+ for (const role of Object.keys(ROLE_PROMPTS)) {
52
+ writeIfMissing(join(agentsDir, `sdods-${role}.agent.md`), copilotAgentContent(role));
53
+ }
54
+ }
55
+ if (target === 'codex' || target === 'copilot' || target === 'all') {
49
56
  writeIfMissing(join(rootDir, 'AGENTS.md'), agentsMdContent(opts));
50
57
  }
51
58
  writeIfMissing(join(rootDir, 'AGENT.md'), agentMdContent());
@@ -89,18 +96,58 @@ ${TAGLINE} Read \`AGENT.md\` for the mental model and \`SKILL.md\` for the comma
89
96
  ${CONVENTIONS.split('\n').slice(3).join('\n')}
90
97
  `;
91
98
  }
92
- /** AGENTS.md: Codex reads this as project instructions; roles become sections. */
99
+ /** A Claude Code subagent (`.claude/agents/sdods-<role>.md`) for one SDODS role. */
100
+ export function claudeAgentContent(role) {
101
+ const p = ROLE_PROMPTS[role];
102
+ return `---\nname: sdods-${role}\ndescription: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.\ntools: Read, Glob, Grep, mcp__sdods__*\n---\n\n${CONVENTIONS}\n\n${p.body}\n`;
103
+ }
104
+ /**
105
+ * The generated role agent files and what this version would write in each, so `sdods upgrade`
106
+ * can tell which ones are out of date. Only `sdods-<role>` files count; nothing else is touched.
107
+ */
108
+ export function codingAgentFiles(rootDir) {
109
+ return Object.keys(ROLE_PROMPTS).flatMap((role) => [
110
+ {
111
+ file: join(rootDir, '.claude', 'agents', `sdods-${role}.md`),
112
+ content: claudeAgentContent(role),
113
+ },
114
+ {
115
+ file: join(rootDir, '.github', 'agents', `sdods-${role}.agent.md`),
116
+ content: copilotAgentContent(role),
117
+ },
118
+ ]);
119
+ }
120
+ /**
121
+ * A GitHub Copilot custom agent (`.github/agents/<name>.agent.md`), used by Copilot CLI
122
+ * (`copilot --agent sdods-healer`), VS Code and the Copilot coding agent. `read` and `search` are
123
+ * Copilot's file tools; `sdods/*` is every tool on the sdods MCP server. No `edit` or `shell`: the
124
+ * roles change the repo only through proposals.
125
+ */
126
+ export function copilotAgentContent(role) {
127
+ const p = ROLE_PROMPTS[role];
128
+ return `---
129
+ name: sdods-${role}
130
+ description: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.
131
+ tools: ['read', 'search', 'sdods/*']
132
+ ---
133
+
134
+ ${CONVENTIONS}
135
+
136
+ ${p.body}
137
+ `;
138
+ }
139
+ /** AGENTS.md: Codex and GitHub Copilot read this as project instructions; roles become sections. */
93
140
  export function agentsMdContent(opts = {}) {
94
141
  const roles = Object.keys(ROLE_PROMPTS)
95
142
  .map((role) => `### sdods-${role}\n\n${ROLE_PROMPTS[role].description}\n\n${ROLE_PROMPTS[role].body}`)
96
143
  .join('\n\n');
97
- return `# AGENTS.md — SDODS instructions for Codex
144
+ return `# AGENTS.md — SDODS instructions for coding agents
98
145
 
99
- ${TAGLINE} This file is read by the OpenAI Codex CLI. \`AGENT.md\` holds the mental model and \`SKILL.md\` the command reference.
146
+ ${TAGLINE} This file is read by the OpenAI Codex CLI, GitHub Copilot and other agents that follow AGENTS.md. \`AGENT.md\` holds the mental model and \`SKILL.md\` the command reference.
100
147
 
101
148
  ## Tools
102
149
 
103
- - The SDODS MCP server is registered as \`sdods\` (\`sdods mcp install codex\` re-registers it in \`~/.codex/config.toml\`); use its tools for projects, features, steps, runs, results, proposals.
150
+ - The SDODS MCP server is registered as \`sdods\` (\`sdods mcp install codex\` re-registers it in \`~/.codex/config.toml\`, \`sdods mcp install copilot\` in \`~/.copilot/mcp-config.json\`); use its tools for projects, features, steps, runs, results, proposals.
104
151
  - Drive a browser with the \`browser_*\` tools on the \`sdods\` server (\`browser_session_open\`, then \`browser_navigate\`, \`browser_snapshot\`, \`browser_click\`). They are the upstream Playwright MCP tools bound to the project and environment, so the page carries the right test-id attribute and login state.
105
152
  - Commands: \`sdods lint${projectFlags(opts)}\`, \`sdods run${projectEnvFlags(opts)} -l api\`, \`sdods run${projectEnvFlags(opts)} -l ui -b chromium -t @smoke\`, \`sdods steps list${projectFlags(opts)}\`, \`sdods proposals list|show|accept\`.
106
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdods/agents",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "SDODS AI agents: planner, generator, healer, upgrader and reviewer over a provider-agnostic adapter.",
5
5
  "license": "Apache-2.0",
6
6
  "funding": "https://sdods.com/sponsor/",
@@ -26,8 +26,8 @@
26
26
  "dependencies": {
27
27
  "@anthropic-ai/claude-agent-sdk": "^0.3.314",
28
28
  "@anthropic-ai/sdk": "^1.3.0",
29
- "@sdods/contracts": "0.19.0",
30
- "@sdods/mcp": "0.19.0",
29
+ "@sdods/contracts": "0.21.0",
30
+ "@sdods/mcp": "0.21.0",
31
31
  "zod": "^4.6.7",
32
32
  "@modelcontextprotocol/client": "^2.4.0"
33
33
  },