Skip to content

Commit 3f851ab

Browse files
committed
feat(agent): add windsurf as an install target
Windsurf (Cascade) reads workspace rules from `.windsurf/rules/*.md`. Add it as an own-file agent target so `testsprite agent install --target windsurf` (and `setup --agent windsurf`) installs the TestSprite skills into a Windsurf project. Reworked onto the v0.2.0 multi-skill agent-targets API (pathFor / SKILLS / DEFAULT_SKILLS). Rule files use Cascade frontmatter with `trigger: model_decision` — the equivalent of the Cursor `.mdc` `alwaysApply: false` mode (description shown up front; full body pulled in on relevance). Budget handling: a `.windsurf/rules/*.md` file caps at ~12 K characters and Cascade silently truncates beyond it, which would cut the full ~22 KB verify skill in half. The windsurf target therefore renders the COMPACT body per skill (new `compactBody` flag + `compactBodyFor`): a skill that ships a trimmed codex asset (`testsprite-verify` → ~5 KB) uses it, while a skill whose codex contribution is only a one-liner (`testsprite-onboard`, ~6.5 KB full) keeps its full body — both land well under the cap. `agent.ts` and `renderForTarget` select the same body so installed bytes match the render. Everything else derives from the TARGETS map automatically (agent list, the setup --agent choices, skill-nudge install detection). Updated the hardcoded help strings, the --help snapshot, the agent-targets/agent unit tests (incl. Cascade-frontmatter and per-skill budget tests), the e2e matrix guards / content-integrity (gated on compactBody), and the README/DOCUMENTATION target lists (incl. the --force own-file list).
1 parent 3ab8136 commit 3f851ab

9 files changed

Lines changed: 174 additions & 29 deletions

File tree

DOCUMENTATION.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,16 @@ testsprite agent install claude # install the skill for Claude Code
112112
testsprite agent install codex # install into AGENTS.md for Codex (managed-section)
113113
testsprite agent install cursor # .cursor/rules/testsprite-verify.mdc
114114
testsprite agent install cline # .clinerules/testsprite-verify.md
115+
testsprite agent install windsurf # .windsurf/rules/testsprite-verify.md
115116
testsprite agent install antigravity # .agents/skills/testsprite-verify/SKILL.md
116-
testsprite agent list # list all 5 targets with status + mode + path
117+
testsprite agent list # list all 6 targets with status + mode + path
117118
```
118119

119-
Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `antigravity` (experimental).
120+
Supported targets: `claude` (GA), `codex` (experimental), `cursor` (experimental), `cline` (experimental), `windsurf` (experimental), `antigravity` (experimental).
120121

121122
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.
122123

123-
Re-running with `--force` on **own-file targets** (claude, cursor, cline, antigravity) backs up the existing file to `<path>.bak` first.
124+
Re-running with `--force` on **own-file targets** (claude, cursor, cline, windsurf, antigravity) backs up the existing file to `<path>.bak` first.
124125

125126
## Command reference
126127

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ npm install -g @testsprite/testsprite-cli
6161
testsprite setup
6262
```
6363

64-
`testsprite setup` prompts for your [API key](https://www.testsprite.com), verifies it, and installs the verification-loop skill for your coding agent (`claude`, `cursor`, `cline`, `antigravity`, `codex`, etc.) — one command, so your agent is wired to verify its own work. Non-interactive (CI / onboarding scripts):
64+
`testsprite setup` prompts for your [API key](https://www.testsprite.com), verifies it, and installs the verification-loop skill for your coding agent (`claude`, `cursor`, `cline`, `windsurf`, `antigravity`, `codex`, etc.) — one command, so your agent is wired to verify its own work. Non-interactive (CI / onboarding scripts):
6565

6666
```bash
6767
TESTSPRITE_API_KEY=sk-... testsprite setup --from-env --yes --agent claude
@@ -110,7 +110,7 @@ Prefer to configure each step by hand (or learn the surface offline with `--dry-
110110
| | `test rerun` | Cheap replay of one/many tests (FE verbatim; BE with deps); `--all --project <id>` reruns all tests |
111111
| | `test wait` | Block on a `runId` until terminal |
112112
| | `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` |
113+
| **Agent** | `agent install` / `agent list` | Add or list coding-agent targets (pure-local): `claude`, `codex`, `cursor`, `cline`, `windsurf`, `antigravity` |
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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -759,12 +759,13 @@ describe('runList', () => {
759759

760760
const json = JSON.parse(capture.stdout.join('\n')) as ListResult[];
761761
expect(Array.isArray(json)).toBe(true);
762-
// 5 targets × 2 default skills = 10 rows
763-
expect(json).toHaveLength(10);
762+
// 6 targets × 2 default skills = 12 rows
763+
expect(json).toHaveLength(12);
764764
const targets = json.map(r => r.target);
765765
expect(targets).toContain('claude');
766766
expect(targets).toContain('cursor');
767767
expect(targets).toContain('cline');
768+
expect(targets).toContain('windsurf');
768769
expect(targets).toContain('antigravity');
769770
expect(targets).toContain('codex');
770771
// skill field present on each row
@@ -908,8 +909,8 @@ describe('createAgentCommand wiring', () => {
908909
// All four own-file targets installed at once
909910
// ---------------------------------------------------------------------------
910911

911-
describe('runInstall — all four own-file targets', () => {
912-
it('installs all four own-file targets in one invocation', async () => {
912+
describe('runInstall — all own-file targets', () => {
913+
it('installs every own-file target in one invocation', async () => {
913914
const { store, fs: agentFs } = makeMemFs();
914915
const { capture, deps } = makeCapture();
915916

@@ -919,7 +920,7 @@ describe('runInstall — all four own-file targets', () => {
919920
output: 'text',
920921
debug: false,
921922
dryRun: false,
922-
target: ['claude', 'cursor', 'cline', 'antigravity'],
923+
target: [...OWN_FILE_TARGETS],
923924
skills: ['testsprite-verify'],
924925
force: false,
925926
},

src/commands/agent.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DEFAULT_SKILLS,
1414
pathFor,
1515
loadSkillBodyFor,
16+
compactBodyFor,
1617
buildCodexAggregate,
1718
renderForTarget,
1819
MANAGED_SECTION_BEGIN,
@@ -434,6 +435,21 @@ export async function runInstall(opts: InstallOptions, deps: AgentDeps = {}): Pr
434435
}
435436
return b;
436437
};
438+
// Budget-capped own-file targets (e.g. windsurf) render the compact per-skill
439+
// body so the rule file isn't truncated by the agent. Cached separately; must
440+
// match renderForTarget's default selection so written bytes equal the asserted
441+
// render.
442+
const compactBodyCache = new Map<string, string>();
443+
const compactBodyForSkill = (skill: string): string => {
444+
let b = compactBodyCache.get(skill);
445+
if (b === undefined) {
446+
b = compactBodyFor(skill);
447+
compactBodyCache.set(skill, b);
448+
}
449+
return b;
450+
};
451+
const ownFileBodyFor = (t: AgentTarget, skill: string): string =>
452+
TARGETS[t].compactBody ? compactBodyForSkill(skill) : bodyForSkill(skill);
437453
let codexSectionCache: string | undefined;
438454
const getCodexSection = (): string => {
439455
if (codexSectionCache === undefined) {
@@ -635,7 +651,7 @@ export async function runInstall(opts: InstallOptions, deps: AgentDeps = {}): Pr
635651
if (abs !== root && !abs.startsWith(root + path.sep)) {
636652
throw new CLIError(`refusing to write outside --dir: ${relPath}`, 5);
637653
}
638-
const content = renderForTarget(t, skill, bodyForSkill(skill)).content;
654+
const content = renderForTarget(t, skill, ownFileBodyFor(t, skill)).content;
639655

640656
if (opts.dryRun) {
641657
const bytes = Buffer.byteLength(content, 'utf8');
@@ -788,7 +804,7 @@ function collect(v: string, prev: string[]): string[] {
788804

789805
export function createAgentCommand(deps: AgentDeps = {}): Command {
790806
const agent = new Command('agent').description(
791-
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Antigravity, Codex)',
807+
'Install TestSprite guidance into coding-agent config (Claude Code, Cursor, Cline, Windsurf, Antigravity, Codex)',
792808
);
793809

794810
agent
@@ -798,7 +814,7 @@ export function createAgentCommand(deps: AgentDeps = {}): Command {
798814
)
799815
.option(
800816
'--target <t>',
801-
'Agent target(s): claude, cursor, cline, antigravity, codex (comma-separated or repeated)',
817+
'Agent target(s): claude, cursor, cline, windsurf, antigravity, codex (comma-separated or repeated)',
802818
collect,
803819
[],
804820
)

src/lib/agent-targets.test.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,19 @@ testsprite test artifact get <run-id> --out ./out/
7474
// ---------------------------------------------------------------------------
7575

7676
describe('TARGETS', () => {
77-
it('has all five required keys', () => {
77+
it('has all six required keys', () => {
7878
const keys = Object.keys(TARGETS).sort();
79-
expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor']);
79+
expect(keys).toEqual(['antigravity', 'claude', 'cline', 'codex', 'cursor', 'windsurf']);
8080
});
8181

8282
it('claude is GA', () => {
8383
expect(TARGETS.claude.status).toBe('ga');
8484
});
8585

86-
it('cursor, cline, antigravity, and codex are experimental', () => {
86+
it('cursor, cline, windsurf, antigravity, and codex are experimental', () => {
8787
expect(TARGETS.cursor.status).toBe('experimental');
8888
expect(TARGETS.cline.status).toBe('experimental');
89+
expect(TARGETS.windsurf.status).toBe('experimental');
8990
expect(TARGETS.antigravity.status).toBe('experimental');
9091
expect(TARGETS.codex.status).toBe('experimental');
9192
});
@@ -102,6 +103,7 @@ describe('TARGETS', () => {
102103
expect(TARGETS.antigravity.mode).toBe('own-file');
103104
expect(TARGETS.cursor.mode).toBe('own-file');
104105
expect(TARGETS.cline.mode).toBe('own-file');
106+
expect(TARGETS.windsurf.mode).toBe('own-file');
105107
});
106108

107109
it('codex target has mode managed-section', () => {
@@ -269,6 +271,52 @@ describe('renderForTarget("cline")', () => {
269271
});
270272
});
271273

274+
describe('renderForTarget("windsurf")', () => {
275+
const result = renderForTarget('windsurf', 'testsprite-verify', STUB_BODY);
276+
277+
it('returns the .windsurf/rules path', () => {
278+
expect(result.path).toBe('.windsurf/rules/testsprite-verify.md');
279+
});
280+
281+
it('uses the Cascade frontmatter (trigger: model_decision + description)', () => {
282+
expect(result.content.startsWith('---\n')).toBe(true);
283+
expect(result.content).toContain('trigger: model_decision');
284+
expect(result.content).toContain(`description: ${SKILL_DESCRIPTION}`);
285+
});
286+
287+
it('does NOT carry the Claude/Cursor frontmatter keys', () => {
288+
const match = /^---\n([\s\S]*?)\n---/.exec(result.content);
289+
const fm = match?.[1] ?? '';
290+
expect(fm).not.toContain('name:'); // claude key
291+
expect(fm).not.toContain('alwaysApply:'); // cursor .mdc key
292+
});
293+
});
294+
295+
describe('windsurf renders within the rules-file budget', () => {
296+
// Regression: a `.windsurf/rules/*.md` file caps at ~12 K characters and
297+
// Cascade silently truncates beyond that. The full verify body (~22 KB) would
298+
// be cut in half, so windsurf renders the COMPACT body for verify (its trimmed
299+
// codex asset) and the full body for onboard (which already fits). Uses the
300+
// REAL bodies (no stub) so the size reflects what a user receives.
301+
for (const skill of DEFAULT_SKILLS) {
302+
it(`${skill} fits under 12 000 characters`, () => {
303+
const r = renderForTarget('windsurf', skill);
304+
expect(r.content.length).toBeLessThan(12_000);
305+
});
306+
}
307+
308+
it('verify uses the compact body (smaller than the full claude render)', () => {
309+
const windsurf = renderForTarget('windsurf', 'testsprite-verify');
310+
const claude = renderForTarget('claude', 'testsprite-verify');
311+
expect(windsurf.content.length).toBeLessThan(claude.content.length);
312+
// The full-body-only intro line is absent from the compact body...
313+
expect(claude.content).toContain('The verification loop that flies');
314+
expect(windsurf.content).not.toContain('The verification loop that flies');
315+
// ...but the load-bearing command survives.
316+
expect(windsurf.content).toContain('testsprite test run');
317+
});
318+
});
319+
272320
// ---------------------------------------------------------------------------
273321
// Content integrity — load-bearing command strings must survive any body trim
274322
// ---------------------------------------------------------------------------

src/lib/agent-targets.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readFileSync } from 'node:fs';
22

3-
export type AgentTarget = 'claude' | 'cursor' | 'cline' | 'antigravity' | 'codex';
3+
export type AgentTarget = 'claude' | 'cursor' | 'cline' | 'antigravity' | 'codex' | 'windsurf';
44

55
export interface TargetSpec {
66
status: 'ga' | 'experimental';
@@ -12,11 +12,19 @@ export interface TargetSpec {
1212
*/
1313
path: string;
1414
/**
15-
* 'own-file': the CLI owns the whole file (claude/cursor/cline/antigravity).
15+
* 'own-file': the CLI owns the whole file (claude/cursor/cline/antigravity/windsurf).
1616
* 'managed-section': the CLI writes only a sentinel-delimited section inside
1717
* a potentially user-authored file (codex target, AGENTS.md).
1818
*/
1919
mode: 'own-file' | 'managed-section';
20+
/**
21+
* When true, render the budget-friendly body (see {@link compactBodyFor})
22+
* instead of the full own-file skill body. Used for own-file targets whose
23+
* rule files are size-capped — currently `windsurf` (`.windsurf/rules/*.md`
24+
* files cap at ~12 K characters and Cascade silently truncates beyond that,
25+
* which would cut the full ~22 KB verify skill in half).
26+
*/
27+
compactBody?: boolean;
2028
/**
2129
* Wrap a skill body in this target's frontmatter/header. Takes the skill's
2230
* `name`+`description` (own-file targets emit them as frontmatter) and the body.
@@ -120,6 +128,18 @@ function wrapMdc(_name: string, description: string, body: string): string {
120128
return `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}\n`;
121129
}
122130

131+
/**
132+
* Windsurf (Cascade) reads workspace rules from `.windsurf/rules/*.md` with YAML
133+
* frontmatter. `trigger: model_decision` is the Cascade equivalent of the Cursor
134+
* `.mdc` `alwaysApply: false` mode: only the `description` is surfaced up front,
135+
* and Cascade pulls in the full rule body when the description shows it is
136+
* relevant — exactly the on-demand activation these skills want. (The other
137+
* triggers are `always_on`, `manual`, and `glob`.)
138+
*/
139+
function wrapWindsurf(_name: string, description: string, body: string): string {
140+
return `---\ntrigger: model_decision\ndescription: ${description}\n---\n\n${body}\n`;
141+
}
142+
123143
// ---------------------------------------------------------------------------
124144
// Landing paths
125145
// ---------------------------------------------------------------------------
@@ -140,6 +160,8 @@ export function pathFor(target: AgentTarget, skill: string): string {
140160
return `.cursor/rules/${skill}.mdc`;
141161
case 'cline':
142162
return `.clinerules/${skill}.md`;
163+
case 'windsurf':
164+
return `.windsurf/rules/${skill}.md`;
143165
case 'codex':
144166
return 'AGENTS.md';
145167
}
@@ -170,6 +192,15 @@ export const TARGETS: Record<AgentTarget, TargetSpec> = {
170192
mode: 'own-file',
171193
wrap: (_name, _description, body) => body,
172194
},
195+
windsurf: {
196+
status: 'experimental',
197+
path: pathFor('windsurf', SKILL_NAME),
198+
mode: 'own-file',
199+
// Windsurf rules files are budget-capped (~12 K chars per `.windsurf/rules/*.md`),
200+
// so render the compact body per skill (see compactBodyFor).
201+
compactBody: true,
202+
wrap: wrapWindsurf,
203+
},
173204
/**
174205
* codex target — managed-section mode.
175206
*
@@ -227,6 +258,24 @@ export function loadSkillBodyFor(skill: string, read: ReadFn = defaultRead): str
227258
return readSkillAsset(spec.bodyFile, read);
228259
}
229260

261+
/**
262+
* Budget-friendly body for an own-file target whose rule files are size-capped
263+
* (e.g. windsurf). For a skill that ships a trimmed codex asset (`codex.kind ===
264+
* 'full'`, e.g. `testsprite-verify` — full body ~22 KB, codex ~5 KB) we render
265+
* that compact asset so the wrapped file stays under the cap. For skills whose
266+
* codex contribution is only a one-liner (`'line'`/`'none'`, e.g.
267+
* `testsprite-onboard`), the one-liner is useless as a standalone rule and the
268+
* full own-file body (~6.5 KB) already fits the budget — so the full body is
269+
* used.
270+
*/
271+
export function compactBodyFor(skill: string, read: ReadFn = defaultRead): string {
272+
const spec = SKILLS[skill];
273+
if (!spec) throw new Error(`unknown skill: ${skill}`);
274+
return spec.codex.kind === 'full'
275+
? readSkillAsset(spec.codex.file, read)
276+
: loadSkillBodyFor(skill, read);
277+
}
278+
230279
/**
231280
* Resolve a skill's codex (AGENTS.md) contribution as a Markdown string.
232281
* 'full' → read the `*.codex.md` asset; 'line' → the inline one-liner; 'none' → ''.
@@ -298,6 +347,7 @@ export function renderForTarget(
298347
const resolvedBody = body !== undefined ? body : codexContentFor(skill);
299348
return { path, content: spec.wrap(skillSpec.name, skillSpec.description, resolvedBody) };
300349
}
301-
const resolvedBody = body !== undefined ? body : loadSkillBodyFor(skill);
350+
const resolvedBody =
351+
body !== undefined ? body : spec.compactBody ? compactBodyFor(skill) : loadSkillBodyFor(skill);
302352
return { path, content: spec.wrap(skillSpec.name, skillSpec.description, resolvedBody) };
303353
}

test/__snapshots__/help.snapshot.test.ts.snap

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`--help snapshots > agent 1`] = `
44
"Usage: testsprite agent [options] [command]
55
66
Install TestSprite guidance into coding-agent config (Claude Code, Cursor,
7-
Cline, Antigravity, Codex)
7+
Cline, Windsurf, Antigravity, Codex)
88
99
Options:
1010
-h, --help display help for command
@@ -25,8 +25,9 @@ Write the TestSprite agent skills (verification loop + first-run onboarding)
2525
into a project for a coding agent
2626
2727
Options:
28-
--target <t> Agent target(s): claude, cursor, cline, antigravity, codex
29-
(comma-separated or repeated) (default: [])
28+
--target <t> Agent target(s): claude, cursor, cline, windsurf,
29+
antigravity, codex (comma-separated or repeated) (default:
30+
[])
3031
--skill <name> Skill(s) to install: testsprite-verify, testsprite-onboard
3132
(comma-separated or repeated; default: all) (default: [])
3233
--dir <path> Project root to write into (default: cwd)
@@ -112,7 +113,8 @@ Options:
112113
--from-env Read TESTSPRITE_API_KEY from the environment instead of
113114
prompting (default: false)
114115
--agent <target> Coding-agent target to install: claude, antigravity,
115-
cursor, cline, codex (default: claude) (default: "claude")
116+
cursor, cline, windsurf, codex (default: claude) (default:
117+
"claude")
116118
--no-agent Skip the agent skill install (configure credentials only)
117119
--force Overwrite an existing skill file (a .bak backup is kept)
118120
--dir <path> Project root for the skill install (default: current
@@ -581,8 +583,8 @@ Commands:
581583
project Manage TestSprite projects
582584
test Inspect TestSprite tests
583585
agent Install TestSprite guidance into coding-agent
584-
config (Claude Code, Cursor, Cline, Antigravity,
585-
Codex)
586+
config (Claude Code, Cursor, Cline, Windsurf,
587+
Antigravity, Codex)
586588
usage|credits Show credit balance and plan/entitlement info
587589
(proactive pre-flight before a large test run)
588590
help [command] display help for command

0 commit comments

Comments
 (0)