From f839518842a7306deca0eb4a02d5c2e1cc3ba262 Mon Sep 17 00:00:00 2001 From: davidson3556 Date: Mon, 6 Jul 2026 00:56:54 -0700 Subject: [PATCH 1/2] feat(agent): add GitHub Copilot as an install target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- DOCUMENTATION.md | 7 +-- README.md | 44 ++++++++-------- src/commands/agent.test.ts | 15 +++--- src/commands/agent.ts | 4 +- src/lib/agent-targets.test.ts | 51 +++++++++++++++++-- src/lib/agent-targets.ts | 30 ++++++++++- test/__snapshots__/help.snapshot.test.ts.snap | 11 ++-- test/e2e/agent-install.e2e.test.ts | 11 +++- test/e2e/setup.e2e.test.ts | 1 + 9 files changed, 130 insertions(+), 44 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index fb62cae..6b798ef 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -115,14 +115,15 @@ testsprite agent install cline # .clinerules/testsprite-verify.md testsprite agent install windsurf # .windsurf/rules/testsprite-verify.md testsprite agent install antigravity # .agents/skills/testsprite-verify/SKILL.md testsprite agent install kiro # .kiro/skills/testsprite-verify/SKILL.md -testsprite agent list # list all 7 targets with status + mode + path +testsprite agent install copilot # .github/instructions/testsprite-verify.instructions.md +testsprite agent list # list all 8 targets with status + mode + path ``` -Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (experimental). +Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental), `kiro` (experimental), `windsurf` (experimental), `copilot` (experimental). 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. -Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf) backs up the existing file to `.bak` first. +Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity, kiro, windsurf, copilot) backs up the existing file to `.bak` first. ## Command reference diff --git a/README.md b/README.md index 79f8aea..404f237 100644 --- a/README.md +++ b/README.md @@ -89,28 +89,28 @@ Prefer to configure each step by hand (or learn the surface offline with `--dry- ## Commands -| Group | Command | What it does | -| --------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| **Setup** | `setup` | **Start here** — one command: configure your API key, verify it, and install the agent verification skill | -| **Auth** | `auth status` | Resolve the active profile to its user, key, env, and scopes | -| | `auth remove` | Remove the active profile from the credentials file | -| **Read** | `project list` / `project get` | List projects / fetch one by id | -| | `test list` / `test get` | List tests under a project / fetch one by id | -| | `test code get` | Print (or write) the generated test source | -| | `test steps` | List the latest run's steps with screenshot / DOM pointers | -| | `test result` | Latest result; `--history` lists a test's prior runs | -| | `test failure get` | The agent entry point: one self-contained latest-failure bundle | -| | `test failure summary` | One-screen triage card (no media download) | -| **Write** | `test create` / `test create-batch` | Create a test (or bulk-create from a plan file); `--produces` / `--needs` / `--category` wire BE dependency metadata | -| | `test update` / `test delete` / `test delete-batch` | Edit metadata / soft-delete | -| | `test code put` | Replace generated code (etag-guarded) | -| | `test plan put` | Replace a frontend test's plan-steps | -| | `project create` / `project update` | Manage projects | -| **Run** | `test run` | Trigger a fresh run; `--wait` blocks until terminal; `--all --project ` runs all tests in a project in wave order | -| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project ` reruns all tests | -| | `test wait` | Block on a `runId` until terminal | -| | `test artifact get` | Download the failure bundle for a specific `runId` | -| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `antigravity`, `kiro`, `windsurf` | +| Group | Command | What it does | +| --------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| **Setup** | `setup` | **Start here** — one command: configure your API key, verify it, and install the agent verification skill | +| **Auth** | `auth status` | Resolve the active profile to its user, key, env, and scopes | +| | `auth remove` | Remove the active profile from the credentials file | +| **Read** | `project list` / `project get` | List projects / fetch one by id | +| | `test list` / `test get` | List tests under a project / fetch one by id | +| | `test code get` | Print (or write) the generated test source | +| | `test steps` | List the latest run's steps with screenshot / DOM pointers | +| | `test result` | Latest result; `--history` lists a test's prior runs | +| | `test failure get` | The agent entry point: one self-contained latest-failure bundle | +| | `test failure summary` | One-screen triage card (no media download) | +| **Write** | `test create` / `test create-batch` | Create a test (or bulk-create from a plan file); `--produces` / `--needs` / `--category` wire BE dependency metadata | +| | `test update` / `test delete` / `test delete-batch` | Edit metadata / soft-delete | +| | `test code put` | Replace generated code (etag-guarded) | +| | `test plan put` | Replace a frontend test's plan-steps | +| | `project create` / `project update` | Manage projects | +| **Run** | `test run` | Trigger a fresh run; `--wait` blocks until terminal; `--all --project ` runs all tests in a project in wave order | +| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project ` reruns all tests | +| | `test wait` | Block on a `runId` until terminal | +| | `test artifact get` | Download the failure bundle for a specific `runId` | +| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `antigravity`, `kiro`, `windsurf`, `copilot` | > 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). diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 7da57cf..4f5fdd3 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -769,13 +769,14 @@ describe('runList', () => { const json = JSON.parse(capture.stdout.join('\n')) as ListResult[]; expect(Array.isArray(json)).toBe(true); - // 7 targets × 2 default skills = 14 rows - expect(json).toHaveLength(14); + // 8 targets × 2 default skills = 16 rows + expect(json).toHaveLength(16); const targets = json.map(r => r.target); expect(targets).toContain('claude'); expect(targets).toContain('cursor'); expect(targets).toContain('cline'); expect(targets).toContain('windsurf'); + expect(targets).toContain('copilot'); expect(targets).toContain('antigravity'); expect(targets).toContain('kiro'); expect(targets).toContain('codex'); @@ -949,11 +950,11 @@ describe('runInstall — all own-file targets', () => { }); // --------------------------------------------------------------------------- -// Dry-run for all six own-file targets +// Dry-run for all seven own-file targets // --------------------------------------------------------------------------- describe('runInstall — dry-run all own-file targets', () => { - it('writes nothing for any of the six own-file targets (default 2 skills = 12 would-write lines)', async () => { + it('writes nothing for any of the seven own-file targets (default 2 skills = 14 would-write lines)', async () => { const { store, fs: agentFs } = makeMemFs(); const { capture, deps } = makeCapture(); @@ -963,7 +964,7 @@ describe('runInstall — dry-run all own-file targets', () => { output: 'text', debug: false, dryRun: true, - target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf'], + target: ['claude', 'cursor', 'cline', 'antigravity', 'kiro', 'windsurf', 'copilot'], force: false, }, { cwd: CWD, fs: agentFs, ...deps }, @@ -973,9 +974,9 @@ describe('runInstall — dry-run all own-file targets', () => { const stderrOut = capture.stderr.join('\n'); // Banner appears once expect(stderrOut).toContain('[dry-run] no files written'); - // 6 targets × 2 default skills = 12 would-write lines + // 7 targets × 2 default skills = 14 would-write lines const wouldWriteLines = stderrOut.split('\n').filter(l => l.includes('would write')); - expect(wouldWriteLines.length).toBe(12); + expect(wouldWriteLines.length).toBe(14); }); }); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 0e80a91..116c015 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -1061,7 +1061,7 @@ function collect(v: string, prev: string[]): string[] { export function createAgentCommand(deps: AgentDeps = {}): Command { const agent = new Command('agent').description( - 'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Antigravity, Codex)', + 'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Copilot, Antigravity, Codex)', ); agent @@ -1071,7 +1071,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command { ) .option( '--target ', - 'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, codex (comma-separated or repeated)', + 'Agent target(s): claude, cursor, cline, antigravity, kiro, windsurf, copilot, codex (comma-separated or repeated)', collect, [], ) diff --git a/src/lib/agent-targets.test.ts b/src/lib/agent-targets.test.ts index 0417166..51bf9b5 100644 --- a/src/lib/agent-targets.test.ts +++ b/src/lib/agent-targets.test.ts @@ -80,19 +80,29 @@ testsprite test artifact get --out ./out/ // --------------------------------------------------------------------------- describe('TARGETS', () => { - it('has all seven required keys', () => { + it('has all eight required keys', () => { const keys = Object.keys(TARGETS).sort(); - expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor', 'kiro', 'windsurf']); + expect(keys).toEqual([ + 'antigravity', + 'claude', + 'cline', + 'codex', + 'copilot', + 'cursor', + 'kiro', + 'windsurf', + ]); }); it('claude is GA', () => { expect(TARGETS.claude.status).toBe('ga'); }); - it('cursor, cline, windsurf, antigravity, kiro, and codex are experimental', () => { + it('cursor, cline, windsurf, copilot, antigravity, kiro, and codex are experimental', () => { expect(TARGETS.cursor.status).toBe('experimental'); expect(TARGETS.cline.status).toBe('experimental'); expect(TARGETS.windsurf.status).toBe('experimental'); + expect(TARGETS.copilot.status).toBe('experimental'); expect(TARGETS.antigravity.status).toBe('experimental'); expect(TARGETS.kiro.status).toBe('experimental'); expect(TARGETS.codex.status).toBe('experimental'); @@ -112,6 +122,7 @@ describe('TARGETS', () => { expect(TARGETS.cline.mode).toBe('own-file'); expect(TARGETS.kiro.mode).toBe('own-file'); expect(TARGETS.windsurf.mode).toBe('own-file'); + expect(TARGETS.copilot.mode).toBe('own-file'); }); it('codex target has mode managed-section', () => { @@ -341,11 +352,45 @@ describe('windsurf renders within the rules-file budget', () => { }); }); +describe('renderForTarget("copilot")', () => { + const result = renderForTarget('copilot', 'testsprite-verify', STUB_BODY); + + it('returns the .github/instructions path', () => { + expect(result.path).toBe('.github/instructions/testsprite-verify.instructions.md'); + }); + + it('uses the Copilot frontmatter (applyTo + description)', () => { + expect(result.content.startsWith('---\n')).toBe(true); + expect(result.content).toContain(`description: ${SKILL_DESCRIPTION}`); + expect(result.content).toContain("applyTo: '**'"); + }); + + it('does NOT carry the Claude/Cursor/Windsurf frontmatter keys', () => { + const match = /^---\n([\s\S]*?)\n---/.exec(result.content); + const fm = match?.[1] ?? ''; + expect(fm).not.toContain('name:'); // claude key + expect(fm).not.toContain('alwaysApply:'); // cursor .mdc key + expect(fm).not.toContain('trigger:'); // windsurf Cascade key + }); + + it('renders the compact verify body (applyTo:** is always-on, so keep it small)', () => { + // Uses the REAL bodies (no stub): copilot always-injects, so like windsurf it + // ships the trimmed verify body while keeping the load-bearing command. + const copilot = renderForTarget('copilot', 'testsprite-verify'); + const claude = renderForTarget('claude', 'testsprite-verify'); + expect(copilot.content.length).toBeLessThan(claude.content.length); + expect(copilot.content).not.toContain('The verification loop that flies'); + expect(copilot.content).toContain('testsprite test run'); + }); +}); + // --------------------------------------------------------------------------- // Content integrity — load-bearing command strings must survive any body trim // --------------------------------------------------------------------------- describe('content integrity — own-file targets', () => { + // Full-body own-file targets. Compact-body targets (windsurf, copilot) are + // excluded — they render the trimmed verify body; see their dedicated tests. const ownFileTargets: Array<'claude' | 'cursor' | 'cline' | 'antigravity' | 'kiro'> = [ 'claude', 'cursor', diff --git a/src/lib/agent-targets.ts b/src/lib/agent-targets.ts index d9a85d5..7e6f94d 100644 --- a/src/lib/agent-targets.ts +++ b/src/lib/agent-targets.ts @@ -9,7 +9,8 @@ export type AgentTarget = | 'antigravity' | 'codex' | 'kiro' - | 'windsurf'; + | 'windsurf' + | 'copilot'; export interface TargetSpec { status: 'ga' | 'experimental'; @@ -149,6 +150,19 @@ function wrapWindsurf(_name: string, description: string, body: string): string return `---\ntrigger: model_decision\ndescription: ${description}\n---\n\n${body}\n`; } +/** + * GitHub Copilot reads path-specific custom instructions from + * `.github/instructions/*.instructions.md` (VS Code / Visual Studio / GitHub + * Copilot Chat). Each file carries YAML frontmatter with `applyTo` — a glob that + * scopes when the instructions attach. `applyTo: '**'` attaches the guidance to + * every request in the repo, which is what a persistent verification skill wants + * (there is no on-demand "model decides" mode for Copilot instruction files, so + * always-apply is the correct idiom). `description` is surfaced in Copilot's UI. + */ +function wrapCopilot(_name: string, description: string, body: string): string { + return `---\ndescription: ${description}\napplyTo: '**'\n---\n\n${body}\n`; +} + // --------------------------------------------------------------------------- // Landing paths // --------------------------------------------------------------------------- @@ -173,6 +187,8 @@ export function pathFor(target: AgentTarget, skill: string): string { return `.kiro/skills/${skill}/SKILL.md`; case 'windsurf': return `.windsurf/rules/${skill}.md`; + case 'copilot': + return `.github/instructions/${skill}.instructions.md`; case 'codex': return 'AGENTS.md'; } @@ -220,6 +236,18 @@ export const TARGETS: Record = { compactBody: true, wrap: wrapWindsurf, }, + copilot: { + status: 'experimental', + path: pathFor('copilot', SKILL_NAME), + mode: 'own-file', + // GitHub Copilot path-specific instructions: frontmatter carries `applyTo`. + // `applyTo: '**'` means the file is ALWAYS injected into Copilot requests + // (there is no on-demand "model decides" mode like Cursor/Windsurf), so + // render the compact body to keep the always-on context cost small — the + // same reasoning that drives windsurf's compact render. + compactBody: true, + wrap: wrapCopilot, + }, /** * codex target — managed-section mode. * diff --git a/test/__snapshots__/help.snapshot.test.ts.snap b/test/__snapshots__/help.snapshot.test.ts.snap index 9fb6b2d..3c6730c 100644 --- a/test/__snapshots__/help.snapshot.test.ts.snap +++ b/test/__snapshots__/help.snapshot.test.ts.snap @@ -4,7 +4,7 @@ exports[`--help snapshots > agent 1`] = ` "Usage: testsprite agent [options] [command] Install TestSprite guidance into coding-agent config (Claude Code, Cursor, -Cline, Windsurf, Antigravity, Codex) +Cline, Windsurf, Copilot, Antigravity, Codex) Options: -h, --help display help for command @@ -29,7 +29,8 @@ into a project for a coding agent Options: --target Agent target(s): claude, cursor, cline, antigravity, kiro, - windsurf, codex (comma-separated or repeated) (default: []) + windsurf, copilot, codex (comma-separated or repeated) + (default: []) --skill Skill(s) to install: testsprite-verify, testsprite-onboard (comma-separated or repeated; default: all) (default: []) --dir Project root to write into (default: cwd) @@ -115,8 +116,8 @@ Options: --from-env Read TESTSPRITE_API_KEY from the environment instead of prompting (default: false) --agent Coding-agent target to install: claude, antigravity, - cursor, cline, kiro, windsurf, codex (default: claude) - (default: "claude") + cursor, cline, kiro, windsurf, copilot, codex (default: + claude) (default: "claude") --no-agent Skip the agent skill install (configure credentials only) --force Overwrite an existing skill file (a .bak backup is kept) --dir Project root for the skill install (default: current @@ -651,7 +652,7 @@ Commands: test Inspect TestSprite tests agent Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, - Antigravity, Codex) + Copilot, Antigravity, Codex) usage|credits Show credit balance and plan/entitlement info (proactive pre-flight before a large test run) help [command] display help for command diff --git a/test/e2e/agent-install.e2e.test.ts b/test/e2e/agent-install.e2e.test.ts index 094aec2..4fb4584 100644 --- a/test/e2e/agent-install.e2e.test.ts +++ b/test/e2e/agent-install.e2e.test.ts @@ -174,6 +174,11 @@ describe('content integrity', () => { expect(content.startsWith('---'), `windsurf: should start with ---`).toBe(true); expect(content).toContain('trigger: model_decision'); expect(content).toContain('description:'); + } else if (target === 'copilot') { + // GitHub Copilot instructions frontmatter: applyTo glob + description + expect(content.startsWith('---'), `copilot: should start with ---`).toBe(true); + expect(content).toContain("applyTo: '**'"); + expect(content).toContain('description:'); } // (b) branding — the renamed H1 must be present in every body variant @@ -211,6 +216,9 @@ describe('content integrity', () => { expect(content.startsWith('---'), `windsurf/onboard: should start with ---`).toBe(true); expect(content).toContain('trigger: model_decision'); expect(content).toContain('description:'); + } else if (target === 'copilot') { + expect(content.startsWith('---'), `copilot/onboard: should start with ---`).toBe(true); + expect(content).toContain("applyTo: '**'"); } // Load-bearing onboard string: the skill body must reference setup @@ -803,7 +811,7 @@ describe('agent list', () => { }>; expect(Array.isArray(parsed)).toBe(true); - // Expected: 7 targets × 2 skills = 14 rows + // Expected: 8 targets × 2 skills = 16 rows const expectedCount = Object.keys(TARGETS).length * DEFAULT_SKILLS.length; expect(parsed.length).toBe(expectedCount); @@ -839,6 +847,7 @@ describe('matrix coverage guard', () => { 'cline', 'kiro', 'windsurf', + 'copilot', 'codex', ]); }); diff --git a/test/e2e/setup.e2e.test.ts b/test/e2e/setup.e2e.test.ts index 3b35751..5a07434 100644 --- a/test/e2e/setup.e2e.test.ts +++ b/test/e2e/setup.e2e.test.ts @@ -229,6 +229,7 @@ describe('matrix coverage guard', () => { 'cline', 'kiro', 'windsurf', + 'copilot', 'codex', ]); }); From 90b6ced92e4c82458370b0168e01df53799216fb Mon Sep 17 00:00:00 2001 From: davidson3556 Date: Mon, 6 Jul 2026 01:17:05 -0700 Subject: [PATCH 2/2] fix(agent): include Kiro in the agent command description The top-level `agent` command description listed the other targets but omitted Kiro (a pre-existing gap), leaving it inconsistent with the `--target` help text and the docs. Align the description with the `--target` list so every supported target is named. --- src/commands/agent.ts | 2 +- test/__snapshots__/help.snapshot.test.ts.snap | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 116c015..0a40c4b 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -1061,7 +1061,7 @@ function collect(v: string, prev: string[]): string[] { export function createAgentCommand(deps: AgentDeps = {}): Command { const agent = new Command('agent').description( - 'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Copilot, Antigravity, Codex)', + 'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Antigravity, Kiro, Windsurf, Copilot, Codex)', ); agent diff --git a/test/__snapshots__/help.snapshot.test.ts.snap b/test/__snapshots__/help.snapshot.test.ts.snap index 3c6730c..ea9dd4d 100644 --- a/test/__snapshots__/help.snapshot.test.ts.snap +++ b/test/__snapshots__/help.snapshot.test.ts.snap @@ -4,7 +4,7 @@ exports[`--help snapshots > agent 1`] = ` "Usage: testsprite agent [options] [command] Install TestSprite guidance into coding-agent config (Claude Code, Cursor, -Cline, Windsurf, Copilot, Antigravity, Codex) +Cline, Antigravity, Kiro, Windsurf, Copilot, Codex) Options: -h, --help display help for command @@ -651,8 +651,8 @@ Commands: project Manage TestSprite projects test Inspect TestSprite tests agent Install TestSprite guidance into coding-agent - config (Claude Code, Cursor, Cline, Windsurf, - Copilot, Antigravity, Codex) + config (Claude Code, Cursor, Cline, Antigravity, + Kiro, Windsurf, Copilot, Codex) usage|credits Show credit balance and plan/entitlement info (proactive pre-flight before a large test run) help [command] display help for command