Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
closes #1650) (#1770)
- Two additive, default-off policy keys under the existing `security:` namespace: `security.integrity.require_hashes` makes `apm install` fail closed when any non-local lockfile entry lacks a content hash, and `security.audit.fail_on_drift` makes `apm audit` exit non-zero when the workspace drifts from the lockfile. Both only tighten through policy inheritance. (#1794)

### Changed

- Windsurf skills now deploy to the cross-tool `.agents/skills/<name>/SKILL.md` path (was `.windsurf/skills/`), converging with Copilot, Cursor, Codex, Gemini, and OpenCode. Pass `--legacy-skill-paths` or set `APM_LEGACY_SKILL_PATHS=1` to restore the per-client `.windsurf/skills/` layout. The lockfile pack-time cross-target skill map for Windsurf is swept separately in #1805. (closes #1520) (#1802)

### Removed

- `apm marketplace publish` command and consumer-repo fan-out workflow; consumers should run `apm install --update` instead. (#1134)
Expand Down
15 changes: 8 additions & 7 deletions docs/src/content/docs/producer/author-primitives/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,23 @@ in `references/`; keep `SKILL.md` to the always-relevant flow.
| Target | Deploy directory |
|-------------------|----------------------------------------------|
| `claude` | `.claude/skills/<name>/SKILL.md` |
| `windsurf` | `.windsurf/skills/<name>/SKILL.md` |
| `kiro` | `.kiro/skills/<name>/SKILL.md` |
| `copilot` | `.agents/skills/<name>/SKILL.md` |
| `cursor` | `.agents/skills/<name>/SKILL.md` |
| `codex` | `.agents/skills/<name>/SKILL.md` |
| `gemini` | `.agents/skills/<name>/SKILL.md` |
| `opencode` | `.agents/skills/<name>/SKILL.md` |
| `windsurf` | `.agents/skills/<name>/SKILL.md` |
| `agent-skills` | `.agents/skills/<name>/SKILL.md` (explicit) |

Five harnesses converge on the cross-tool `.agents/skills/`
directory. Claude keeps its harness-native path because Claude Code's
default scan is `.claude/skills/`; Windsurf and Kiro currently use
`.windsurf/skills/` and `.kiro/skills/` for the same reason. Windsurf's Cascade also
Six harnesses converge on the cross-tool `.agents/skills/`
directory. Claude and Kiro keep their harness-native paths
(`.claude/skills/`, `.kiro/skills/`) because those clients' default
scan is the per-tool directory. Windsurf (now Devin Desktop) converged
onto `.agents/skills/` in
[#1520](https://github.com/microsoft/apm/issues/1520): Cascade
[discovers `.agents/skills/`](https://docs.windsurf.com/windsurf/cascade/skills#skill-scopes)
natively for cross-agent compatibility (convergence tracked in
[#1520](https://github.com/microsoft/apm/issues/1520)). The whole
natively, and Devin's own docs use `.agents/skills/`. The whole
skill folder is copied (`shutil.copytree`), so `scripts/`,
`references/`, `assets/`, and `examples/` ride along. Symlinks and
the `.apm-pin` cache marker are filtered out
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/manifest-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ A plural alias `targets:` (YAML list only) is also accepted and takes precedence
| `codex` | Emits `AGENTS.md` and deploys skills to `.agents/skills/`, agents to `.codex/agents/`. |
| `gemini` | Emits `GEMINI.md` and deploys to `.gemini/commands/`, `.gemini/skills/`, `.gemini/settings.json`. |
| `antigravity` | Emits `AGENTS.md` and deploys rules to `.agents/rules/`, skills to `.agents/skills/`, hooks to `.agents/hooks.json`, MCP to `.agents/mcp_config.json`. Explicit-only (not auto-detected; not part of `--target all`). |
| `windsurf` | Emits `AGENTS.md` and deploys to `.windsurf/rules/`, `.windsurf/skills/`, `.windsurf/workflows/`, `.windsurf/hooks.json`. |
| `windsurf` | Emits `AGENTS.md` and deploys to `.windsurf/rules/`, `.agents/skills/`, `.windsurf/workflows/`, `.windsurf/hooks.json`. |
| `kiro` | Emits `AGENTS.md` and deploys to `.kiro/steering/`, `.kiro/skills/`, `.kiro/hooks/`, `.kiro/settings/mcp.json`. |
| `all` | All targets. Cannot be combined with other values in a list. |
| `minimal` | `AGENTS.md` only at project root. **Auto-detected only**: this value MUST NOT be set explicitly in manifests; it is an internal fallback when no target folder is detected. |
Expand Down
10 changes: 5 additions & 5 deletions docs/src/content/docs/reference/targets-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ see [Primitive types](./primitive-types/).
| gemini | `.gemini/` | [ ] | [ ] | [ ] | [x] | [x] | [x] | [x] |
| antigravity | `.agents/` | [x] | [ ] | [ ] | [x] | [ ] | [x] | [x] |
| opencode | `.opencode/` | [ ] | [ ] | [x] | [x] | [x] | [ ] | [x] |
| windsurf | `.windsurf/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] |
| windsurf | `.windsurf/` + `.agents/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] |
| kiro | `.kiro/` | [x] | [ ] | [ ] | [x] | [ ] | [x] | [x] |
| agent-skills | `.agents/` | [ ] | [ ] | [ ] | [x] | [ ] | [ ] | [ ] |

Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode,
Gemini, Antigravity, and Codex by default (see [Skills convergence](#skills-convergence)
below). Claude, Windsurf, and Kiro keep target-native skill directories.
Gemini, Antigravity, Codex, and Windsurf by default (see [Skills convergence](#skills-convergence)
below). Claude and Kiro keep target-native skill directories.

`copilot-cowork` (Microsoft 365 Copilot), `copilot-app` (GitHub
Copilot desktop App), and `openclaw` (OpenClaw agent runtime) are
Expand Down Expand Up @@ -180,11 +180,11 @@ OpenCode.
Windsurf / Cascade.

- **Detection.** `.windsurf/` directory.
- **Deploy directory.** `.windsurf/` at project scope; `~/.codeium/windsurf/` at user scope.
- **Deploy directory.** Native primitives deploy under `.windsurf/` at project scope and `~/.codeium/windsurf/` at user scope; skills converge on `.agents/skills/` at both scopes (`~/.agents/skills/` at user scope).
- **Supported primitives.** instructions, skills, commands, hooks, mcp.
- **File conventions.**
- instructions: `.windsurf/rules/<name>.md`
- skills: `.windsurf/skills/<name>/SKILL.md`
- skills: `.agents/skills/<name>/SKILL.md`
- commands: `.windsurf/workflows/<name>.md`
- hooks: `.windsurf/hooks.json`
- **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.
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/core/target_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ def get_target_description(target: UserTargetType) -> str:
"codex": "AGENTS.md + .agents/skills/ + .codex/agents/ + .codex/hooks.json",
"gemini": "GEMINI.md + .gemini/commands/ + .gemini/skills/ + .gemini/settings.json (MCP/hooks)",
"antigravity": "AGENTS.md + .agents/rules/ + .agents/skills/ + .agents/hooks.json + .agents/mcp_config.json (explicit --target only)",
"windsurf": "AGENTS.md + .windsurf/rules/ + .windsurf/skills/ + .windsurf/workflows/ + .windsurf/hooks.json",
"windsurf": "AGENTS.md + .windsurf/rules/ + .agents/skills/ + .windsurf/workflows/ + .windsurf/hooks.json",
"kiro": "AGENTS.md + .kiro/steering/ + .kiro/skills/ + .kiro/hooks/ + .kiro/settings/mcp.json",
"agent-skills": ".agents/skills/ only (cross-client shared skills -- no agents, hooks, or commands)",
"openclaw": ".agents/skills/ (project) or ~/.openclaw/skills/ (--global) -- experimental",
Expand Down
17 changes: 13 additions & 4 deletions src/apm_cli/integration/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,10 +708,14 @@ def for_scope(self, user_scope: bool = False) -> TargetProfile | None:
compile_family="agents",
hooks_config_display=".codex/hooks.json",
),
# Windsurf/Cascade -- .windsurf/ is the workspace config directory.
# Windsurf/Cascade (now Devin Desktop) -- .windsurf/ is the workspace
# config directory.
# Rules are markdown files with trigger/globs frontmatter under .windsurf/rules/.
# Skills use the standard SKILL.md format under .windsurf/skills/.
# Cascade auto-invokes them when the description frontmatter matches the
# Skills converge onto the cross-tool .agents/skills/<name>/SKILL.md path
# (deploy_root=".agents"), matching copilot/cursor/codex/gemini/opencode;
# Devin's own docs also use .agents/skills/. Rules, workflows, and hooks
# stay under .windsurf/.
# Cascade auto-invokes skills when the description frontmatter matches the
# task -- this is the universal invocation mechanism, so windsurf does
# NOT expose a separate ``agents`` primitive. Package authors who want
# their content to deploy to windsurf must declare it under
Expand All @@ -734,7 +738,12 @@ def for_scope(self, user_scope: bool = False) -> TargetProfile | None:
"windsurf_rules",
output_compare=True,
),
"skills": PrimitiveMapping("skills", "/SKILL.md", "skill_standard"),
"skills": PrimitiveMapping(
"skills",
"/SKILL.md",
"skill_standard",
deploy_root=".agents",
),
"commands": PrimitiveMapping("workflows", ".md", "windsurf_workflow"),
"hooks": PrimitiveMapping("", "hooks.json", "windsurf_hooks"),
},
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/integration/test_data_driven_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def test_partition_parity_with_old_buckets(self):
"agents_opencode",
"agents_codex",
# NOTE: windsurf no longer exposes an 'agents' primitive
# (its content deploys as skills under .windsurf/skills/).
# (its content deploys as skills under .agents/skills/).
"commands", # was commands_claude, aliased
"commands_cursor",
"commands_gemini",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/integration/test_scope_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def test_project_scope_uses_windsurf_root(self):
assert "instructions" in resolved.primitives
assert "skills" in resolved.primitives
# windsurf intentionally does not expose an 'agents' primitive:
# Cascade discovers SKILL.md uniformly under .windsurf/skills/.
# Cascade discovers SKILL.md uniformly under .agents/skills/.
assert "agents" not in resolved.primitives

def test_user_scope_uses_codeium_windsurf_root(self):
Expand Down
44 changes: 39 additions & 5 deletions tests/unit/integration/test_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,21 +305,22 @@ def test_copilot_profile_lists_root_generated_file(self):


# ---------------------------------------------------------------------------
# Skill routing convergence (convergence §1)
# Skill routing convergence (apm#1520)
# ---------------------------------------------------------------------------


class TestDefaultSkillRouting:
"""Assert that the 4 documented clients route skills to .agents/ by default."""
"""Assert that the documented clients route skills to .agents/ by default."""

def test_default_skill_routing_uses_agents_dir_for_documented_clients(self):
"""copilot, cursor, opencode, codex, gemini all have deploy_root='.agents' on skills."""
"""copilot, cursor, opencode, codex, gemini, windsurf all have deploy_root='.agents' on skills."""
expected = {
"copilot": ".agents",
"cursor": ".agents",
"opencode": ".agents",
"codex": ".agents",
"gemini": ".agents",
"windsurf": ".agents",
"claude": None, # not documented as .agents/-aware
}
for name, want_root in expected.items():
Expand All @@ -335,11 +336,12 @@ def test_legacy_skill_paths_flag_restores_per_client_routing(self):
from apm_cli.integration.targets import apply_legacy_skill_paths

profiles = [
KNOWN_TARGETS[n] for n in ("copilot", "cursor", "opencode", "codex", "claude", "gemini")
KNOWN_TARGETS[n]
for n in ("copilot", "cursor", "opencode", "codex", "claude", "gemini", "windsurf")
]
restored = apply_legacy_skill_paths(profiles)

# All 6 should have deploy_root=None after legacy restore
# All 7 should have deploy_root=None after legacy restore
for profile in restored:
skills_pm = profile.primitives.get("skills")
assert skills_pm is not None, f"{profile.name} should have skills"
Expand Down Expand Up @@ -374,6 +376,38 @@ def test_gemini_legacy_skill_paths_restores_per_client_routing(self):
f"gemini: expected deploy_root=None (legacy), got {skills_pm.deploy_root!r}"
)

def test_windsurf_skill_routing_uses_agents_dir_by_default(self):
"""Windsurf (now Devin Desktop) converges skills onto .agents/skills/ (apm#1520)."""
profile = KNOWN_TARGETS["windsurf"]
skills_pm = profile.primitives["skills"]
assert skills_pm.deploy_root == ".agents", (
f"windsurf: expected deploy_root='.agents', got {skills_pm.deploy_root!r}"
)
# The deploy directory is <effective_root>/skills/<name>; with the
# override that resolves to .agents/skills/ (parts, not substring).
effective_root = skills_pm.deploy_root or profile.root_dir
resolved = Path(effective_root) / "skills"
assert resolved.parts == (".agents", "skills"), (
f"windsurf: expected .agents/skills/, got {resolved.parts!r}"
)

def test_windsurf_legacy_skill_paths_restores_per_client_routing(self):
"""With apply_legacy_skill_paths(), windsurf skills return to .windsurf/skills/ (apm#1520)."""
from apm_cli.integration.targets import apply_legacy_skill_paths

profiles = [KNOWN_TARGETS["windsurf"]]
restored = apply_legacy_skill_paths(profiles)
profile = restored[0]
skills_pm = profile.primitives["skills"]
assert skills_pm.deploy_root is None, (
f"windsurf: expected deploy_root=None (legacy), got {skills_pm.deploy_root!r}"
)
effective_root = skills_pm.deploy_root or profile.root_dir
resolved = Path(effective_root) / "skills"
assert resolved.parts == (".windsurf", "skills"), (
f"windsurf legacy: expected .windsurf/skills/, got {resolved.parts!r}"
)

def test_apply_legacy_does_not_mutate_known_targets(self):
"""apply_legacy_skill_paths must not mutate the global KNOWN_TARGETS."""
from apm_cli.integration.targets import apply_legacy_skill_paths
Expand Down
Loading
Loading