Skip to content

Commit f839518

Browse files
committed
feat(agent): add GitHub Copilot as an install target
Adds `copilot` to the agent-install targets. GitHub Copilot reads path-specific custom instructions from `.github/instructions/*.instructions.md` (VS Code / Visual Studio / Copilot Chat), with YAML frontmatter carrying an `applyTo` glob. The skill installs to `.github/instructions/testsprite-verify.instructions.md` (and the onboard skill alongside) with `applyTo: '**'` so the guidance attaches to every request in the repo. Because `applyTo: '**'` is always-on (Copilot has no on-demand 'model decides' mode like Cursor/Windsurf), the target renders the COMPACT body — the same reasoning behind windsurf's compact render — keeping the always-injected context small (~6 KB vs the ~23 KB full body). Slots into the existing TARGETS machinery, so `agent list`, `setup --agent`, and skill-nudge install-detection pick it up automatically. Updates the AgentTarget union, pathFor, TARGETS, help/docs, unit + e2e matrix guards, and the help snapshot. Fixes #193
1 parent b9e9601 commit f839518

9 files changed

Lines changed: 130 additions & 44 deletions

File tree

DOCUMENTATION.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,15 @@ testsprite agent install cline # .clinerules/testsprite-verify.md
115115
testsprite agent install windsurf # .windsurf/rules/testsprite-verify.md
116116
testsprite agent install antigravity # .agents/skills/testsprite-verify/SKILL.md
117117
testsprite agent install kiro # .kiro/skills/testsprite-verify/SKILL.md
118-
testsprite agent list # list all 7 targets with status + mode + path
118+
testsprite agent install copilot # .github/instructions/testsprite-verify.instructions.md
119+
testsprite agent list # list all 8 targets with status + mode + path
119120
```
120121

121-
Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (experimental).
122+
Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (experimental), `copilot` (experimental).
122123

123124
The `codex` target uses **managed-section mode** — it writes only a sentinel-delimited section inside your existing `AGENTS.md`, so your project instructions are never clobbered. Re-running without `--force` replaces the section in-place; user content outside the sentinels is always preserved.
124125

125-
Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf) backs up the existing file to `<path>.bak` first.
126+
Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf, copilot) backs up the existing file to `<path>.bak` first.
126127

127128
## Command reference
128129

README.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,28 +89,28 @@ Prefer to configure each step by hand (or learn the surface offline with `--dry-
8989

9090
## Commands
9191

92-
| Group | Command | What it does |
93-
| --------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
94-
| **Setup** | `setup` | **Start here** — one command: configure your API key, verify it, and install the agent verification skill |
95-
| **Auth** | `auth status` | Resolve the active profile to its user, key, env, and scopes |
96-
| | `auth remove` | Remove the active profile from the credentials file |
97-
| **Read** | `project list` / `project get` | List projects / fetch one by id |
98-
| | `test list` / `test get` | List tests under a project / fetch one by id |
99-
| | `test code get` | Print (or write) the generated test source |
100-
| | `test steps` | List the latest run's steps with screenshot / DOM pointers |
101-
| | `test result` | Latest result; `--history` lists a test's prior runs |
102-
| | `test failure get` | The agent entry point: one self-contained latest-failure bundle |
103-
| | `test failure summary` | One-screen triage card (no media download) |
104-
| **Write** | `test create` / `test create-batch` | Create a test (or bulk-create from a plan file); `--produces` / `--needs` / `--category` wire BE dependency metadata |
105-
| | `test update` / `test delete` / `test delete-batch` | Edit metadata / soft-delete |
106-
| | `test code put` | Replace generated code (etag-guarded) |
107-
| | `test plan put` | Replace a frontend test's plan-steps |
108-
| | `project create` / `project update` | Manage projects |
109-
| **Run** | `test run` | Trigger a fresh run; `--wait` blocks until terminal; `--all --project <id>` runs all tests in a project in wave order |
110-
| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project <id>` reruns all tests |
111-
| | `test wait` | Block on a `runId` until terminal |
112-
| | `test artifact get` | Download the failure bundle for a specific `runId` |
113-
| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `antigravity`, `kiro`, `windsurf` |
92+
| Group | Command | What it does |
93+
| --------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
94+
| **Setup** | `setup` | **Start here** — one command: configure your API key, verify it, and install the agent verification skill |
95+
| **Auth** | `auth status` | Resolve the active profile to its user, key, env, and scopes |
96+
| | `auth remove` | Remove the active profile from the credentials file |
97+
| **Read** | `project list` / `project get` | List projects / fetch one by id |
98+
| | `test list` / `test get` | List tests under a project / fetch one by id |
99+
| | `test code get` | Print (or write) the generated test source |
100+
| | `test steps` | List the latest run's steps with screenshot / DOM pointers |
101+
| | `test result` | Latest result; `--history` lists a test's prior runs |
102+
| | `test failure get` | The agent entry point: one self-contained latest-failure bundle |
103+
| | `test failure summary` | One-screen triage card (no media download) |
104+
| **Write** | `test create` / `test create-batch` | Create a test (or bulk-create from a plan file); `--produces` / `--needs` / `--category` wire BE dependency metadata |
105+
| | `test update` / `test delete` / `test delete-batch` | Edit metadata / soft-delete |
106+
| | `test code put` | Replace generated code (etag-guarded) |
107+
| | `test plan put` | Replace a frontend test's plan-steps |
108+
| | `project create` / `project update` | Manage projects |
109+
| **Run** | `test run` | Trigger a fresh run; `--wait` blocks until terminal; `--all --project <id>` runs all tests in a project in wave order |
110+
| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project <id>` reruns all tests |
111+
| | `test wait` | Block on a `runId` until terminal |
112+
| | `test artifact get` | Download the failure bundle for a specific `runId` |
113+
| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `antigravity`, `kiro`, `windsurf`, `copilot` |
114114

115115
> The earlier command names — `init`, `auth configure`, `auth whoami`, `auth logout` — still work as hidden, deprecated aliases (each prints a one-line notice pointing at the new name), so existing scripts keep running. `auth configure` now runs the full `setup` (it also installs the skill).
116116

src/commands/agent.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -769,13 +769,14 @@ describe('runList', () => {
769769

770770
const json = JSON.parse(capture.stdout.join('\n')) as ListResult[];
771771
expect(Array.isArray(json)).toBe(true);
772-
// 7 targets × 2 default skills = 14 rows
773-
expect(json).toHaveLength(14);
772+
// 8 targets × 2 default skills = 16 rows
773+
expect(json).toHaveLength(16);
774774
const targets = json.map(r => r.target);
775775
expect(targets).toContain('claude');
776776
expect(targets).toContain('cursor');
777777
expect(targets).toContain('cline');
778778
expect(targets).toContain('windsurf');
779+
expect(targets).toContain('copilot');
779780
expect(targets).toContain('antigravity');
780781
expect(targets).toContain('kiro');
781782
expect(targets).toContain('codex');
@@ -949,11 +950,11 @@ describe('runInstall — all own-file targets', () => {
949950
});
950951

951952
// ---------------------------------------------------------------------------
952-
// Dry-run for all six own-file targets
953+
// Dry-run for all seven own-file targets
953954
// ---------------------------------------------------------------------------
954955

955956
describe('runInstall — dry-run all own-file targets', () => {
956-
it('writes nothing for any of the six own-file targets (default 2 skills = 12 would-write lines)', async () => {
957+
it('writes nothing for any of the seven own-file targets (default 2 skills = 14 would-write lines)', async () => {
957958
const { store, fs: agentFs } = makeMemFs();
958959
const { capture, deps } = makeCapture();
959960

@@ -963,7 +964,7 @@ describe('runInstall — dry-run all own-file targets', () => {
963964
output: 'text',
964965
debug: false,
965966
dryRun: true,
966-
target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf'],
967+
target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf', 'copilot'],
967968
force: false,
968969
},
969970
{ cwd: CWD, fs: agentFs, ...deps },
@@ -973,9 +974,9 @@ describe('runInstall — dry-run all own-file targets', () => {
973974
const stderrOut = capture.stderr.join('\n');
974975
// Banner appears once
975976
expect(stderrOut).toContain('[dry-run] no files written');
976-
// 6 targets × 2 default skills = 12 would-write lines
977+
// 7 targets × 2 default skills = 14 would-write lines
977978
const wouldWriteLines = stderrOut.split('\n').filter(l => l.includes('would write'));
978-
expect(wouldWriteLines.length).toBe(12);
979+
expect(wouldWriteLines.length).toBe(14);
979980
});
980981
});
981982

src/commands/agent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ function collect(v: string, prev: string[]): string[] {
10611061

10621062
export function createAgentCommand(deps: AgentDeps = {}): Command {
10631063
const agent = new Command('agent').description(
1064-
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Antigravity, Codex)',
1064+
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Copilot, Antigravity, Codex)',
10651065
);
10661066

10671067
agent
@@ -1071,7 +1071,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command {
10711071
)
10721072
.option(
10731073
'--target <t>',
1074-
'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, codex (comma-separated or repeated)',
1074+
'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, copilot, codex (comma-separated or repeated)',
10751075
collect,
10761076
[],
10771077
)

src/lib/agent-targets.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,29 @@ testsprite test artifact get <run-id> --out ./out/
8080
// ---------------------------------------------------------------------------
8181

8282
describe('TARGETS', () => {
83-
it('has all seven required keys', () => {
83+
it('has all eight required keys', () => {
8484
const keys = Object.keys(TARGETS).sort();
85-
expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor', 'kiro', 'windsurf']);
85+
expect(keys).toEqual([
86+
'antigravity',
87+
'claude',
88+
'cline',
89+
'codex',
90+
'copilot',
91+
'cursor',
92+
'kiro',
93+
'windsurf',
94+
]);
8695
});
8796

8897
it('claude is GA', () => {
8998
expect(TARGETS.claude.status).toBe('ga');
9099
});
91100

92-
it('cursor, cline, windsurf, antigravity, kiro, and codex are experimental', () => {
101+
it('cursor, cline, windsurf, copilot, antigravity, kiro, and codex are experimental', () => {
93102
expect(TARGETS.cursor.status).toBe('experimental');
94103
expect(TARGETS.cline.status).toBe('experimental');
95104
expect(TARGETS.windsurf.status).toBe('experimental');
105+
expect(TARGETS.copilot.status).toBe('experimental');
96106
expect(TARGETS.antigravity.status).toBe('experimental');
97107
expect(TARGETS.kiro.status).toBe('experimental');
98108
expect(TARGETS.codex.status).toBe('experimental');
@@ -112,6 +122,7 @@ describe('TARGETS', () => {
112122
expect(TARGETS.cline.mode).toBe('own-file');
113123
expect(TARGETS.kiro.mode).toBe('own-file');
114124
expect(TARGETS.windsurf.mode).toBe('own-file');
125+
expect(TARGETS.copilot.mode).toBe('own-file');
115126
});
116127

117128
it('codex target has mode managed-section', () => {
@@ -341,11 +352,45 @@ describe('windsurf renders within the rules-file budget', () => {
341352
});
342353
});
343354

355+
describe('renderForTarget("copilot")', () => {
356+
const result = renderForTarget('copilot', 'testsprite-verify', STUB_BODY);
357+
358+
it('returns the .github/instructions path', () => {
359+
expect(result.path).toBe('.github/instructions/testsprite-verify.instructions.md');
360+
});
361+
362+
it('uses the Copilot frontmatter (applyTo + description)', () => {
363+
expect(result.content.startsWith('---\n')).toBe(true);
364+
expect(result.content).toContain(`description: ${SKILL_DESCRIPTION}`);
365+
expect(result.content).toContain("applyTo: '**'");
366+
});
367+
368+
it('does NOT carry the Claude/Cursor/Windsurf frontmatter keys', () => {
369+
const match = /^---\n([\s\S]*?)\n---/.exec(result.content);
370+
const fm = match?.[1] ?? '';
371+
expect(fm).not.toContain('name:'); // claude key
372+
expect(fm).not.toContain('alwaysApply:'); // cursor .mdc key
373+
expect(fm).not.toContain('trigger:'); // windsurf Cascade key
374+
});
375+
376+
it('renders the compact verify body (applyTo:** is always-on, so keep it small)', () => {
377+
// Uses the REAL bodies (no stub): copilot always-injects, so like windsurf it
378+
// ships the trimmed verify body while keeping the load-bearing command.
379+
const copilot = renderForTarget('copilot', 'testsprite-verify');
380+
const claude = renderForTarget('claude', 'testsprite-verify');
381+
expect(copilot.content.length).toBeLessThan(claude.content.length);
382+
expect(copilot.content).not.toContain('The verification loop that flies');
383+
expect(copilot.content).toContain('testsprite test run');
384+
});
385+
});
386+
344387
// ---------------------------------------------------------------------------
345388
// Content integrity — load-bearing command strings must survive any body trim
346389
// ---------------------------------------------------------------------------
347390

348391
describe('content integrity — own-file targets', () => {
392+
// Full-body own-file targets. Compact-body targets (windsurf, copilot) are
393+
// excluded — they render the trimmed verify body; see their dedicated tests.
349394
const ownFileTargets: Array<'claude' | 'cursor' | 'cline' | 'antigravity' | 'kiro'> = [
350395
'claude',
351396
'cursor',

src/lib/agent-targets.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export type AgentTarget =
99
| 'antigravity'
1010
| 'codex'
1111
| 'kiro'
12-
| 'windsurf';
12+
| 'windsurf'
13+
| 'copilot';
1314

1415
export interface TargetSpec {
1516
status: 'ga' | 'experimental';
@@ -149,6 +150,19 @@ function wrapWindsurf(_name: string, description: string, body: string): string
149150
return `---\ntrigger: model_decision\ndescription: ${description}\n---\n\n${body}\n`;
150151
}
151152

153+
/**
154+
* GitHub Copilot reads path-specific custom instructions from
155+
* `.github/instructions/*.instructions.md` (VS Code / Visual Studio / GitHub
156+
* Copilot Chat). Each file carries YAML frontmatter with `applyTo` — a glob that
157+
* scopes when the instructions attach. `applyTo: '**'` attaches the guidance to
158+
* every request in the repo, which is what a persistent verification skill wants
159+
* (there is no on-demand "model decides" mode for Copilot instruction files, so
160+
* always-apply is the correct idiom). `description` is surfaced in Copilot's UI.
161+
*/
162+
function wrapCopilot(_name: string, description: string, body: string): string {
163+
return `---\ndescription: ${description}\napplyTo: '**'\n---\n\n${body}\n`;
164+
}
165+
152166
// ---------------------------------------------------------------------------
153167
// Landing paths
154168
// ---------------------------------------------------------------------------
@@ -173,6 +187,8 @@ export function pathFor(target: AgentTarget, skill: string): string {
173187
return `.kiro/skills/${skill}/SKILL.md`;
174188
case 'windsurf':
175189
return `.windsurf/rules/${skill}.md`;
190+
case 'copilot':
191+
return `.github/instructions/${skill}.instructions.md`;
176192
case 'codex':
177193
return 'AGENTS.md';
178194
}
@@ -220,6 +236,18 @@ export const TARGETS: Record<AgentTarget, TargetSpec> = {
220236
compactBody: true,
221237
wrap: wrapWindsurf,
222238
},
239+
copilot: {
240+
status: 'experimental',
241+
path: pathFor('copilot', SKILL_NAME),
242+
mode: 'own-file',
243+
// GitHub Copilot path-specific instructions: frontmatter carries `applyTo`.
244+
// `applyTo: '**'` means the file is ALWAYS injected into Copilot requests
245+
// (there is no on-demand "model decides" mode like Cursor/Windsurf), so
246+
// render the compact body to keep the always-on context cost small — the
247+
// same reasoning that drives windsurf's compact render.
248+
compactBody: true,
249+
wrap: wrapCopilot,
250+
},
223251
/**
224252
* codex target — managed-section mode.
225253
*

0 commit comments

Comments
 (0)