Skip to content

Commit 38f8640

Browse files
yoelabriljaloncadyoelabrildanielmeppielCopilot
authored
Fix/windsurf uninstall skills cleanup (#1486)
* fix(windsurf): remove agents primitive to fix skills cleanup on uninstall The windsurf TargetProfile declared both 'agents' and 'skills' primitives mapped to the same deploy path .windsurf/skills/<name>/SKILL.md. This caused BaseIntegrator.partition_managed_files to route uninstall paths to the agents_windsurf bucket, where AgentIntegrator.sync_for_target called unlink() on what was actually a directory. The IsADirectoryError was swallowed silently and skill directories survived 'apm uninstall'. Drop the 'agents' primitive from windsurf entirely. Cascade auto-invokes any SKILL.md under .windsurf/skills/ based on the description frontmatter, so a separate agents primitive was redundant and was the only source of the path collision. Also remove the now-unreachable _write_windsurf_agent_skill transformer in AgentIntegrator and its dedicated unit tests, and update the windsurf scope integration tests to assert the new shape. Closes #1481 * fix(windsurf): remove agents primitive to fix skills cleanup on uninstall The windsurf TargetProfile declared both 'agents' and 'skills' primitives mapped to the same deploy path .windsurf/skills/<name>/SKILL.md. This caused BaseIntegrator.partition_managed_files to route uninstall paths to the agents_windsurf bucket, where AgentIntegrator.sync_for_target called unlink() on what was actually a directory. The IsADirectoryError was swallowed silently and skill directories survived 'apm uninstall'. Drop the 'agents' primitive from windsurf entirely. Cascade auto-invokes any SKILL.md under .windsurf/skills/ based on the description frontmatter, so a separate agents primitive was redundant and was the only source of the path collision. Also remove the now-unreachable _write_windsurf_agent_skill transformer in AgentIntegrator and its dedicated unit tests, and update the windsurf scope integration tests to assert the new shape. Closes #1481 * test(scope): mark agents as unsupported at user scope for windsurf Windsurf has no native agents primitive, so supports_at_user_scope("agents") should return False. Update test assertion and clarify comment. * docs(agents): document removal of windsurf agent deployment path Earlier APM versions compiled `.apm/agents/*.agent.md` to `.windsurf/skills/<name>/SKILL.md` with stripped frontmatter. That mapping has been removed: agents no longer deploy to Windsurf. Add migration caution block advising users to re-author personas as skills under `.apm/skills/<name>/SKILL.md` if they need Windsurf support. Update CHANGELOG.md to document the uninstall cleanup fix for the shared deploy path that caused empty directories to survive. * fix(windsurf): fold Copilot review nits on PR #1486 - instruction_integrator.py: reflow the yaml.safe_load comment into a single clear sentence so it no longer reads as an orphan continuation. - test_windsurf_uninstall_skills.py: pivot the regression tests to assert filesystem outcomes primarily (managed dirs removed, user dir preserved). The 'files_removed' stats key is kept as a sanity check because it is the established cross-integrator counter convention (see base_integrator, hook_integrator, command_integrator, prompt_integrator, skill_integrator, copilot_app_workflow_integrator, agent_integrator) and is consumed by uninstall/engine.py counters; renaming would have repo-wide blast radius for a directory-counting edge case that affects only the windsurf skills path. Co-authored-by: yoelabril <abril.yoel@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(changelog): tighten #1486 entry and link convergence follow-up #1520 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Alonso Cadierno Juan <juan.alonsocadierno@gruposantander.com> Co-authored-by: yoelabril <abril.yoel@gmail.com> Co-authored-by: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com> Co-authored-by: Daniel Meppiel <danielmeppiel@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1bc2edc commit 38f8640

15 files changed

Lines changed: 199 additions & 453 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- `apm install --update` now re-resolves direct git-source semver dependencies. Previously, when the dependency's install path already existed on disk, the BFS resolver short-circuited and `--update` was a silent no-op for git-semver refs; the lockfile kept the previously-resolved tag.
3535
- `apm unpack` deprecation banner softened from "will be removed in v0.14" to "will be removed in a future release". The previous wording shipped past v0.14.0 and contradicted reality (the command still ships in v0.15.0). A concrete removal version will be re-stated once a removal milestone is set.
3636
- `policy.dependencies.require_pinned_constraint: true` no longer misclassifies the npm- and cargo-style explicit-equality form `=1.2.3` as `BARE_BRANCH`. Both `1.2.3` and `=1.2.3` are now recognized as pinned constraints; the pip-style `==1.2.3` form is still rejected (not part of node-semver). Follow-up to #1494 / #1505.
37+
- `apm uninstall` now fully cleans `.windsurf/skills/<pkg>/` directories on the `windsurf` target (by @yoelabril, #1486). The deploy path itself will move to `.agents/skills/` per Cascade's native cross-agent discovery -- tracked in #1520.
3738

3839
## [0.15.0] - 2026-05-27
3940

docs/src/content/docs/concepts/primitives-and-targets.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Notes per target:
9090
- **codex** -- Codex CLI. Agents and hooks use TOML; skills use the cross-tool `.agents/` directory.
9191
- **gemini** -- Gemini CLI. Commands are TOML. Hooks merge into `.gemini/settings.json`. No native agents or instructions primitives -- both arrive via compiled context files.
9292
- **opencode** -- OpenCode. No hooks support.
93-
- **windsurf** -- Windsurf / Cascade. Agents are delivered as auto-invokable skills under `.windsurf/skills/`. Workflows are the harness's name for commands.
93+
- **windsurf** -- Windsurf / Cascade. No native agents primitive -- Cascade auto-invokes any `SKILL.md` by its `description:` frontmatter, so personas ship as skills. Workflows are the harness's name for commands.
9494

9595
## The compatibility matrix
9696

@@ -105,7 +105,7 @@ Rows are primitives, columns are harnesses. Cell legend:
105105
|---|---|---|---|---|---|---|---|
106106
| instructions | native | native | native | compiled | compiled | compiled | native |
107107
| prompts | native | compiled | compiled | unsupported | compiled | compiled | compiled |
108-
| agents | native | native | compiled | compiled | unsupported | native | compiled |
108+
| agents | native | native | compiled | compiled | unsupported | native | unsupported |
109109
| skills | native | native | native | native | native | native | native |
110110
| hooks | native | native | native | native | native | unsupported | native |
111111
| commands | unsupported | native | compiled | unsupported | compiled | compiled | compiled |

docs/src/content/docs/integrations/ide-tool-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The full slot-by-slot capability table lives in [Targets matrix](../reference/ta
2121
| Codex CLI | `.codex/` | Skills, MCP |
2222
| Gemini CLI | `.gemini/` or `GEMINI.md` | Single-file or distributed |
2323
| OpenCode | `.opencode/` | Skills, MCP |
24-
| Windsurf | `.windsurf/` | Rules + MCP (lossy agent->skill) |
24+
| Windsurf | `.windsurf/` | Rules + Skills + Workflows + MCP |
2525
| Agent-Skills (cross) | `.agents/skills/` | Vendor-neutral skill sharing |
2626

2727
For exact per-target capabilities (which primitives are supported, transformer used, file layout), see [Targets matrix](../reference/targets-matrix/).

docs/src/content/docs/producer/author-primitives/instructions-and-agents.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ for...
145145
| `color` | optional | Display color for harnesses that render it (Copilot, Claude, OpenCode). OpenCode requires a `#rgb`/`#rrggbb` hex literal or one of its theme names; see "Common pitfalls" below |
146146

147147
`model` and `tools` reach Copilot, Claude, Cursor, and OpenCode
148-
verbatim. Codex receives a TOML translation. Windsurf drops both
149-
fields and emits a diagnostic warning at install time -- its skill
150-
format does not support per-persona model or tool scoping.
148+
verbatim. Codex receives a TOML translation. Windsurf and Gemini do
149+
not receive `.agent.md` files at all -- Cascade auto-invokes any
150+
`SKILL.md` by its description, and Gemini CLI has no agents primitive.
151151

152152
OpenCode is the strictest of the verbatim targets: it requires
153153
`tools` as a `tool-name: boolean` **mapping** (not a list, not a
@@ -179,9 +179,20 @@ offending package and field so you can fix the source.
179179
| cursor | `.cursor/agents/<name>.md` | verbatim |
180180
| opencode | `.opencode/agents/<name>.md` | verbatim |
181181
| codex | `.codex/agents/<name>.toml` | YAML frontmatter -> TOML; body becomes `developer_instructions` |
182-
| windsurf | `.windsurf/skills/<name>/SKILL.md` | reformatted as a Cascade skill; `model`/`tools` dropped with a warning |
182+
| windsurf | not deployed | Windsurf has no agents primitive -- author personas as skills (Cascade auto-invokes by description) |
183183
| gemini | not deployed | Gemini CLI has no agents primitive |
184184

185+
:::caution[Migration]
186+
Earlier APM versions compiled `.apm/agents/*.agent.md` to
187+
`.windsurf/skills/<name>/SKILL.md` with `model` and `tools`
188+
frontmatter stripped. That mapping has been removed: agents no
189+
longer deploy to Windsurf at all. If you previously relied on it
190+
and the persona still needs to reach Windsurf, re-author it as a
191+
skill under `.apm/skills/<name>/SKILL.md` -- Cascade auto-invokes
192+
skills by their `description` field, which is the same surface
193+
the agent path was using.
194+
:::
195+
185196
Source: `src/apm_cli/integration/agent_integrator.py`,
186197
`src/apm_cli/integration/targets.py`.
187198

@@ -212,10 +223,10 @@ dedicated persona.
212223
will not bind.
213224
- **Agent named `default` or `start`.** These collide with script
214225
resolution in `apm run`. Pick a descriptive name.
215-
- **`model:` and `tools:` on a Windsurf-targeted agent.** Cascade has
216-
no equivalent; APM warns and drops them. If those constraints are
217-
load-bearing, do not target windsurf for that agent -- ship it as
218-
an instruction instead, or restrict the package's `targets:`.
226+
- **Targeting an agent at Windsurf or Gemini.** Neither harness has an
227+
agents primitive. Cascade auto-invokes skills by description and
228+
Gemini folds context into `GEMINI.md`. If a persona must reach those
229+
targets, author it as a skill under `.apm/skills/<name>/SKILL.md`.
219230
- **`tools:` as a list, or a named color, on an OpenCode-targeted
220231
agent.** OpenCode's loader rejects `tools: [Read, Grep]` and
221232
colors like `cyan`. Use the mapping form (`tools: {Read: true}`)

docs/src/content/docs/reference/targets-matrix.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ see [Primitive types](../primitive-types/).
2525
| codex | `.codex/` + `.agents/` | [ ] | [ ] | [x] | [x] | [ ] | [x] | [x] |
2626
| gemini | `.gemini/` | [ ] | [ ] | [ ] | [x] | [x] | [x] | [x] |
2727
| opencode | `.opencode/` | [ ] | [ ] | [x] | [x] | [x] | [ ] | [x] |
28-
| windsurf | `.windsurf/` | [x] | [ ] | [x] | [x] | [x] | [x] | [x] |
28+
| windsurf | `.windsurf/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] |
2929
| agent-skills | `.agents/` | [ ] | [ ] | [ ] | [x] | [ ] | [ ] | [ ] |
3030

3131
Skills always deploy to the cross-tool `.agents/skills/` directory by
@@ -158,13 +158,13 @@ Windsurf / Cascade.
158158

159159
- **Detection.** `.windsurf/` directory.
160160
- **Deploy directory.** `.windsurf/` at project scope; `~/.codeium/windsurf/` at user scope.
161-
- **Supported primitives.** instructions, agents, skills, commands, hooks, mcp.
161+
- **Supported primitives.** instructions, skills, commands, hooks, mcp.
162162
- **File conventions.**
163163
- instructions: `.windsurf/rules/<name>.md`
164-
- agents: `.windsurf/skills/<name>/SKILL.md` (Cascade auto-invokes skill-shaped agents by description)
165164
- skills: `.windsurf/skills/<name>/SKILL.md`
166165
- commands: `.windsurf/workflows/<name>.md`
167166
- hooks: `.windsurf/hooks.json`
167+
- **Agents.** Not deployed. Cascade auto-invokes any `SKILL.md` by its `description:` frontmatter, so a separate agents primitive would collide with skills on the same path. Ship personas as skills under `.apm/skills/<name>/SKILL.md` instead.
168168
- **User scope.** Partial. `instructions` is excluded at user scope; Windsurf stores global memory in a single `~/.codeium/windsurf/memories/global_rules.md` file with a different format.
169169

170170
## agent-skills

src/apm_cli/integration/agent_integrator.py

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,6 @@ def integrate_agents_for_target(
168168
if mapping.format_id == "codex_agent":
169169
self._write_codex_agent(source_file, target_path)
170170
links_resolved = 0
171-
elif mapping.format_id == "windsurf_agent_skill":
172-
links_resolved = self._write_windsurf_agent_skill(
173-
source_file, target_path, diagnostics=diagnostics
174-
)
175171
else:
176172
if mapping.format_id == "opencode_agent":
177173
self._warn_opencode_frontmatter(
@@ -338,77 +334,6 @@ def _write_codex_agent(source: Path, target: Path) -> None:
338334
}
339335
target.write_text(_toml.dumps(doc), encoding="utf-8")
340336

341-
# ------------------------------------------------------------------
342-
# Windsurf agent-skill transformer (agent.md -> skills/<name>/SKILL.md)
343-
# ------------------------------------------------------------------
344-
345-
def _write_windsurf_agent_skill(
346-
self, source: Path, target: Path, diagnostics=None
347-
) -> int: # not @staticmethod: needs self.resolve_links()
348-
"""Transform an ``.agent.md`` file to a Windsurf Skill (``SKILL.md``).
349-
350-
Windsurf Skills are the closest equivalent to a specialist persona:
351-
- Invocable with ``@skill-name`` (like ``@agent-name`` in Copilot)
352-
- Auto-invoked by Cascade when the description matches the task
353-
- Support a directory with supplementary resource files
354-
355-
The conversion:
356-
- Keeps ``name`` (or derives from filename) and ``description``.
357-
- Strips agent-specific keys (``model``, ``tools``) and emits a
358-
diagnostic warning when those fields are dropped.
359-
- Preserves the markdown body verbatim.
360-
"""
361-
if source.is_symlink():
362-
raise ValueError(f"Refusing to read symlink source: {source}")
363-
content = source.read_text(encoding="utf-8")
364-
365-
stem = source.name
366-
if stem.endswith(".agent.md"):
367-
stem = stem[:-9]
368-
elif stem.endswith(".chatmode.md"):
369-
stem = stem[:-12]
370-
else:
371-
stem = Path(stem).stem
372-
373-
fm_match = AgentIntegrator._FRONTMATTER_RE.match(content)
374-
if fm_match:
375-
body = content[fm_match.end() :]
376-
try:
377-
fm = yaml.safe_load(fm_match.group(1)) or {}
378-
except Exception:
379-
fm = {}
380-
else:
381-
body = content
382-
fm = {}
383-
384-
dropped = [k for k in ("tools", "model") if fm.get(k)]
385-
if dropped and diagnostics is not None:
386-
diagnostics.warn(
387-
f"Windsurf skill conversion dropped frontmatter field(s) "
388-
f"{', '.join(dropped)} from {source.name}",
389-
detail="Windsurf Skills do not support agent-only fields; "
390-
"only name, description, and body are preserved.",
391-
)
392-
393-
name = fm.get("name", stem)
394-
description = fm.get("description", "")
395-
396-
# Use yaml.safe_dump to safely serialize values -- prevents YAML key
397-
# injection via multi-line name/description strings.
398-
399-
fm_data: dict = {"name": name}
400-
if description:
401-
fm_data["description"] = description
402-
fm_yaml = yaml.safe_dump( # yaml-io-exempt: serializes to string, not file handle
403-
fm_data, default_flow_style=False, allow_unicode=True
404-
).rstrip("\n")
405-
406-
result = f"---\n{fm_yaml}\n---\n" + body
407-
result, links_resolved = self.resolve_links(result, source, target)
408-
target.parent.mkdir(parents=True, exist_ok=True)
409-
target.write_text(result, encoding="utf-8")
410-
return links_resolved
411-
412337
# DEPRECATED: use integrate_agents_for_target(KNOWN_TARGETS["copilot"], ...) instead.
413338
def integrate_package_agents(
414339
self,

src/apm_cli/integration/instruction_integrator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,7 @@ def _convert_to_windsurf_rules(content: str) -> str:
359359
body = content
360360
apply_to = ""
361361

362-
# Parse existing frontmatter with yaml.safe_load (consistent with
363-
# _write_windsurf_agent_skill and all other frontmatter parsers).
362+
# Parse existing frontmatter with yaml.safe_load for consistency with the other frontmatter parsers across integrators.
364363
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n?", content, re.DOTALL)
365364
if fm_match:
366365
body = content[fm_match.end() :]

src/apm_cli/integration/targets.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,12 @@ def for_scope(self, user_scope: bool = False) -> TargetProfile | None:
530530
),
531531
# Windsurf/Cascade -- .windsurf/ is the workspace config directory.
532532
# Rules are markdown files with trigger/globs frontmatter under .windsurf/rules/.
533-
# Agents are deployed as skills under .windsurf/skills/<name>/SKILL.md
534-
# (Cascade auto-invokes them when the description matches the task).
535533
# Skills use the standard SKILL.md format under .windsurf/skills/.
534+
# Cascade auto-invokes them when the description frontmatter matches the
535+
# task -- this is the universal invocation mechanism, so windsurf does
536+
# NOT expose a separate ``agents`` primitive. Package authors who want
537+
# their content to deploy to windsurf must declare it under
538+
# ``.apm/skills/<name>/SKILL.md`` (not under ``.apm/agents/``).
536539
# Workflows (~= commands) are markdown files under .windsurf/workflows/.
537540
# Hooks are configured in .windsurf/hooks.json.
538541
# At user scope, ~/.codeium/windsurf/ is used. Global rules use a single
@@ -546,7 +549,6 @@ def for_scope(self, user_scope: bool = False) -> TargetProfile | None:
546549
root_dir=".windsurf",
547550
primitives={
548551
"instructions": PrimitiveMapping("rules", ".md", "windsurf_rules"),
549-
"agents": PrimitiveMapping("skills", "/SKILL.md", "windsurf_agent_skill"),
550552
"skills": PrimitiveMapping("skills", "/SKILL.md", "skill_standard"),
551553
"commands": PrimitiveMapping("workflows", ".md", "windsurf_workflow"),
552554
"hooks": PrimitiveMapping("", "hooks.json", "windsurf_hooks"),

tests/integration/test_integrators_validation_phase3b.py

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -610,57 +610,10 @@ def test_write_codex_agent_rejects_symlink(self, tmp_path: Path) -> None:
610610
AgentIntegrator._write_codex_agent(link, dst)
611611

612612

613-
class TestAgentIntegratorWriteWindsurfAgentSkill:
614-
"""_write_windsurf_agent_skill transforms .agent.md to SKILL.md."""
615-
616-
def test_write_windsurf_basic(self, tmp_path: Path) -> None:
617-
src = tmp_path / "tester.agent.md"
618-
dst = tmp_path / "SKILL.md"
619-
src.write_text("# Tester Agent\n\nTest things.", encoding="utf-8")
620-
integrator = AgentIntegrator()
621-
integrator._write_windsurf_agent_skill(src, dst)
622-
content = dst.read_text(encoding="utf-8")
623-
assert "---" in content
624-
assert "tester" in content.lower()
625-
626-
def test_write_windsurf_with_frontmatter(self, tmp_path: Path) -> None:
627-
src = tmp_path / "rev.agent.md"
628-
dst = tmp_path / "SKILL.md"
629-
src.write_text(
630-
"---\nname: Reviewer\ndescription: Reviews code\n---\n\nReview all PRs.",
631-
encoding="utf-8",
632-
)
633-
integrator = AgentIntegrator()
634-
integrator._write_windsurf_agent_skill(src, dst)
635-
content = dst.read_text(encoding="utf-8")
636-
assert "Reviewer" in content
637-
assert "Reviews code" in content
638-
639-
def test_write_windsurf_drops_agent_only_fields(self, tmp_path: Path) -> None:
640-
"""tools and model fields are dropped with a diagnostic."""
641-
src = tmp_path / "agent.agent.md"
642-
dst = tmp_path / "SKILL.md"
643-
src.write_text(
644-
"---\nname: SomeAgent\ntools:\n - read_file\nmodel: gpt-4\n---\n\nBody.",
645-
encoding="utf-8",
646-
)
647-
diagnostics = MagicMock()
648-
integrator = AgentIntegrator()
649-
integrator._write_windsurf_agent_skill(src, dst, diagnostics=diagnostics)
650-
content = dst.read_text(encoding="utf-8")
651-
# tools and model should not appear in the output YAML frontmatter
652-
assert "tools:" not in content or "SomeAgent" in content
653-
diagnostics.warn.assert_called_once()
654-
655-
def test_write_windsurf_rejects_symlink(self, tmp_path: Path) -> None:
656-
real = tmp_path / "real.agent.md"
657-
real.write_text("content", encoding="utf-8")
658-
link = tmp_path / "link.agent.md"
659-
link.symlink_to(real)
660-
dst = tmp_path / "SKILL.md"
661-
integrator = AgentIntegrator()
662-
with pytest.raises(ValueError, match="symlink"):
663-
integrator._write_windsurf_agent_skill(link, dst)
613+
# NOTE: TestAgentIntegratorWriteWindsurfAgentSkill was removed when windsurf
614+
# dropped its 'agents' primitive. The agents -> SKILL.md transformer is no
615+
# longer reachable from production code; windsurf deploys SKILL.md directly
616+
# via the 'skills' primitive.
664617

665618

666619
class TestAgentIntegratorIntegrateForTarget:

tests/integration/test_integrators_validation_rules.py

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -610,57 +610,10 @@ def test_write_codex_agent_rejects_symlink(self, tmp_path: Path) -> None:
610610
AgentIntegrator._write_codex_agent(link, dst)
611611

612612

613-
class TestAgentIntegratorWriteWindsurfAgentSkill:
614-
"""_write_windsurf_agent_skill transforms .agent.md to SKILL.md."""
615-
616-
def test_write_windsurf_basic(self, tmp_path: Path) -> None:
617-
src = tmp_path / "tester.agent.md"
618-
dst = tmp_path / "SKILL.md"
619-
src.write_text("# Tester Agent\n\nTest things.", encoding="utf-8")
620-
integrator = AgentIntegrator()
621-
integrator._write_windsurf_agent_skill(src, dst)
622-
content = dst.read_text(encoding="utf-8")
623-
assert "---" in content
624-
assert "tester" in content.lower()
625-
626-
def test_write_windsurf_with_frontmatter(self, tmp_path: Path) -> None:
627-
src = tmp_path / "rev.agent.md"
628-
dst = tmp_path / "SKILL.md"
629-
src.write_text(
630-
"---\nname: Reviewer\ndescription: Reviews code\n---\n\nReview all PRs.",
631-
encoding="utf-8",
632-
)
633-
integrator = AgentIntegrator()
634-
integrator._write_windsurf_agent_skill(src, dst)
635-
content = dst.read_text(encoding="utf-8")
636-
assert "Reviewer" in content
637-
assert "Reviews code" in content
638-
639-
def test_write_windsurf_drops_agent_only_fields(self, tmp_path: Path) -> None:
640-
"""tools and model fields are dropped with a diagnostic."""
641-
src = tmp_path / "agent.agent.md"
642-
dst = tmp_path / "SKILL.md"
643-
src.write_text(
644-
"---\nname: SomeAgent\ntools:\n - read_file\nmodel: gpt-4\n---\n\nBody.",
645-
encoding="utf-8",
646-
)
647-
diagnostics = MagicMock()
648-
integrator = AgentIntegrator()
649-
integrator._write_windsurf_agent_skill(src, dst, diagnostics=diagnostics)
650-
content = dst.read_text(encoding="utf-8")
651-
# tools and model should not appear in the output YAML frontmatter
652-
assert "tools:" not in content or "SomeAgent" in content
653-
diagnostics.warn.assert_called_once()
654-
655-
def test_write_windsurf_rejects_symlink(self, tmp_path: Path) -> None:
656-
real = tmp_path / "real.agent.md"
657-
real.write_text("content", encoding="utf-8")
658-
link = tmp_path / "link.agent.md"
659-
link.symlink_to(real)
660-
dst = tmp_path / "SKILL.md"
661-
integrator = AgentIntegrator()
662-
with pytest.raises(ValueError, match="symlink"):
663-
integrator._write_windsurf_agent_skill(link, dst)
613+
# NOTE: TestAgentIntegratorWriteWindsurfAgentSkill was removed when windsurf
614+
# dropped its 'agents' primitive. The agents -> SKILL.md transformer is no
615+
# longer reachable from production code; windsurf deploys SKILL.md directly
616+
# via the 'skills' primitive.
664617

665618

666619
class TestAgentIntegratorIntegrateForTarget:

0 commit comments

Comments
 (0)