Skip to content

Feature/windsurf target#1066

Merged
danielmeppiel merged 22 commits intomicrosoft:mainfrom
yoelabril:feature/windsurf-target
May 2, 2026
Merged

Feature/windsurf target#1066
danielmeppiel merged 22 commits intomicrosoft:mainfrom
yoelabril:feature/windsurf-target

Conversation

@yoelabril
Copy link
Copy Markdown
Contributor

Description

Add Windsurf/Cascade as a first-class APM compilation and integration target.

Windsurf is a VS Code-based AI IDE with its own configuration layout (.windsurf/). This PR adds full support so that apm install --target windsurf and apm compile --target windsurf deploy primitives into the correct locations.

Primitive mapping:

APM primitive Windsurf destination Notes
instructions .windsurf/rules/*.md applyTotrigger: glob frontmatter conversion
agents .windsurf/skills/<name>/SKILL.md No native agent primitive; Skills are the closest semantic equivalent
skills .windsurf/skills/<name>/SKILL.md Native SKILL.md format
commands/prompts .windsurf/workflows/*.md Slash-command invoked
hooks .windsurf/hooks.json
compiled AGENTS.md AGENTS.md (root) Windsurf reads it natively (always-on)

Why agents → skills? Windsurf has no agent primitive. Skills are the closest match: invocable with @skill-name, auto-activated by Cascade based on description, and support supplementary resource files. Tool boundaries (tools:) and model selection (model:) are not preserved as Windsurf does not support them at the skill level.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

New tests added:

  • TestWindsurfAgentSkillConversion — 5 unit tests for the _write_windsurf_agent_skill transformer
  • TestWindsurfAgentSkillIntegration — 4 integration tests for end-to-end deployment
  • TestConvertToWindsurfRules — 6 unit tests for instruction frontmatter conversion
  • TestWindsurfRulesIntegration — 2 integration tests for rules deployment
  • Target detection, scope resolution, and MCP runtime tests

Note: 2 pre-existing test failures on Windows (TestClaudeScopeResolution::test_user_scope_outside_home_keeps_absolute and test_user_scope_collapses_dotdot_segments) are unrelated to this PR — they fail on upstream main due to Path.home() not respecting monkeypatched HOME on Windows.

Copilot AI review requested due to automatic review settings April 30, 2026 10:52
@yoelabril
Copy link
Copy Markdown
Contributor Author

@yoelabril please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Windsurf/Cascade as a first-class APM target so apm install/compile --target windsurf can deploy primitives into .windsurf/ (and user-scope config under ~/.codeium/windsurf/ where applicable).

Changes:

  • Registers a new windsurf TargetProfile and target auto-detection/CLI parsing support.
  • Adds instruction and agent integration behavior for Windsurf (rules frontmatter conversion; agents -> skills).
  • Introduces a Windsurf MCP client adapter and extends MCP runtime install/cleanup logic to include Windsurf.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/unit/integration/test_targets.py Extends active target detection tests to include .windsurf/.
tests/unit/integration/test_scope_integration.py Adds Windsurf scope resolution tests and user-scope primitive filtering assertions.
tests/unit/integration/test_instruction_integrator.py Adds unit + integration tests for Windsurf rules conversion/deploy.
tests/unit/integration/test_agent_integrator.py Adds unit + integration tests for agents -> Windsurf skills conversion/deploy.
tests/unit/core/test_target_detection.py Adds detection + compile-behavior tests for the windsurf target.
tests/unit/core/test_scope.py Updates KNOWN_TARGETS expectations and user-scope support assertions for Windsurf.
src/apm_cli/integration/targets.py Registers windsurf target mapping and scope metadata.
src/apm_cli/integration/mcp_integrator.py Adds Windsurf to MCP runtime detection and stale-config cleanup.
src/apm_cli/integration/instruction_integrator.py Adds windsurf_rules instruction conversion + copy path.
src/apm_cli/integration/agent_integrator.py Adds windsurf_agent_skill transformer and dispatch.
src/apm_cli/factory.py Registers WindsurfClientAdapter in ClientFactory.
src/apm_cli/core/target_detection.py Adds windsurf to target types, auto-detect, and descriptions.
src/apm_cli/compilation/agents_compiler.py Allows compiling for the windsurf target.
src/apm_cli/adapters/client/windsurf.py New MCP client adapter writing ~/.codeium/windsurf/mcp_config.json.
build/apm.spec Adds the Windsurf client module to PyInstaller hidden imports.

Comment thread src/apm_cli/integration/targets.py Outdated
Comment thread src/apm_cli/integration/mcp_integrator.py
Comment thread tests/unit/integration/test_instruction_integrator.py Outdated
Comment thread src/apm_cli/core/target_detection.py Outdated
Comment thread src/apm_cli/integration/instruction_integrator.py
Comment thread src/apm_cli/integration/targets.py Outdated
Comment thread src/apm_cli/integration/instruction_integrator.py Outdated
Comment thread src/apm_cli/core/target_detection.py Outdated
Comment thread src/apm_cli/integration/targets.py Outdated
Comment thread src/apm_cli/adapters/client/windsurf.py Outdated
@danielmeppiel danielmeppiel added status/accepted Direction approved, safe to start work. panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Five specialists agreed on 13 required findings across security, architecture, docs, and UX. The Windsurf integration is strategically important but structurally incomplete and must not merge in its current state.

Required before merge (13 items)

  • [python-architect] configure_mcp_server duplicated verbatim in WindsurfClientAdapter despite docstring claiming it delegates to parent at src/apm_cli/adapters/client/windsurf.py:87
    • Why: The only difference from CopilotClientAdapter is the print label. Any future bug fix in the parent silently will not apply to Windsurf. Docstring is actively misleading.
    • Suggested fix: Delete the method body. Call super().configure_mcp_server(...) and replace just the print label. Or add a protected _client_label property to CopilotClientAdapter used in the parent print() call.
  • [python-architect] windsurf_hooks format_id declared in TargetProfile but hook integration silently returns 0 -- no hooks are ever deployed at src/apm_cli/integration/hook_integrator.py:153
    • Why: 'windsurf' is absent from _MERGE_HOOK_TARGETS (only claude/cursor/codex/gemini registered). Any package shipping windsurf hooks installs successfully but deploys zero hook files with no error or warning.
    • Suggested fix: Add windsurf to _MERGE_HOOK_TARGETS with appropriate _MergeHookConfig, or remove the hooks primitive from windsurf TargetProfile and document the gap.
  • [cli-logging-expert] configure_mcp_server uses raw print() for all four output paths, bypassing rich* helpers and STATUS_SYMBOLS at src/apm_cli/adapters/client/windsurf.py:97
    • Why: Breaks traffic-light coloring and quiet/verbose mode. The parent CopilotClientAdapter has the same bug, but this PR duplicates and extends that anti-pattern to a new surface. Must use _rich_error for errors and _rich_success for the success path.
  • [devx-ux-expert] windsurf not listed as valid --target value anywhere in cli-commands.md at docs/src/content/docs/reference/cli-commands.md
    • Why: Lines 94, 587, 1005, and 1667 enumerate valid targets; windsurf is absent from all four. A user running apm compile --help or reading the reference docs has no way to discover windsurf as a valid target. Per DevX UX rule: if a CLI change is not reflected in cli-commands.md in the same PR, that change is incomplete by definition.
  • [devx-ux-expert] packages/apm-guide shipped skill commands.md has zero mentions of windsurf at packages/apm-guide/.apm/skills/apm-usage/commands.md
    • Why: AI assistants given the apm-guide skill will have no knowledge that windsurf is a valid target, producing incorrect guidance to users. Must stay in sync with CLI surface per Rule 4.
  • [devx-ux-expert] Hooks silently not deployed for windsurf -- no _MERGE_HOOK_TARGETS entry at src/apm_cli/integration/hook_integrator.py
    • Why: Violates the 'failure mode is the product' principle. Users get no error, no warning, just silently missing hooks. Either remove hooks from windsurf TargetProfile or add windsurf to _MERGE_HOOK_TARGETS.
  • [devx-ux-expert] get_target_description('windsurf') says '.windsurf/rules/ (instructions + agents)' but agents actually deploy to .windsurf/skills/ at src/apm_cli/core/target_detection.py
    • Why: Description is shown to users and will cause confusion when they look for their agents in the wrong directory. The parenthetical '(instructions + agents)' next to '.windsurf/rules/' is factually wrong for agents.
  • [supply-chain-security-expert] Frontmatter injection via unescaped apply_to in _convert_to_windsurf_rules at src/apm_cli/integration/instruction_integrator.py:362
    • Why: apply_to is extracted from package-supplied YAML and inserted verbatim as f'globs: {apply_to}'. A malicious package can ship applyTo with embedded newlines to inject extra frontmatter fields -- including overriding trigger to make rules fire unconditionally without user consent.
    • Suggested fix: Strip or reject any value containing newlines before interpolation, or quote-escape the value so newlines cannot break the YAML line boundary.
  • [supply-chain-security-expert] YAML key injection via name and description in _write_windsurf_agent_skill at src/apm_cli/integration/agent_integrator.py:251
    • Why: yaml.safe_load parses source frontmatter safely, but extracted scalars are then written with bare f-strings. Multi-line description values inject arbitrary YAML keys into the Windsurf skill frontmatter (e.g., auto_invoke: true, tools: [bash]).
    • Suggested fix: Serialize via yaml.dump({'name': name, 'description': description}, default_flow_style=False) or strip embedded newlines before interpolation.
  • [supply-chain-security-expert] Missing ensure_path_within on instruction target_path before write in copy_instruction_windsurf at src/apm_cli/integration/instruction_integrator.py:111
    • Why: target_path is constructed from package-supplied filename stem and written to disk without calling ensure_path_within(target_path, project_root). Policy requires every path derived from package-controlled input to pass through the path-security guards before any disk write, consistent with other integrators.
    • Suggested fix: Add ensure_path_within(target_path, project_root) after constructing target_path and before dispatching to the format-specific copy function.
  • [oss-growth-hacker] No CHANGELOG.md entry for windsurf support at CHANGELOG.md
    • Why: Adding a new IDE compilation target is a top-tier user-visible feature. Missing CHANGELOG entry is a missed launch beat for existing users deciding whether to upgrade and share. Should appear under [Unreleased] Added.
  • [oss-growth-hacker] README.md lists supported IDEs but omits windsurf in three places at README.md
    • Why: Lines 7, 62, and 140 enumerate supported targets (Copilot, Claude, Cursor, OpenCode, Codex, Gemini). Windsurf is absent from all three. Windsurf/Codeium users will not see themselves reflected and will bounce.
  • [oss-growth-hacker] manifest-schema.md target allowed-values table omits windsurf at docs/src/content/docs/reference/manifest-schema.md
    • Why: Table explicitly enumerates every valid target token. If windsurf is a valid target value it must appear here, or users and tooling validators will treat it as unknown and raise a parse error -- actively breaking users who try target: windsurf in apm.yml.

Nits (8 items, skip if you want)

  • [python-architect] update_config and get_current_config duplicated in WindsurfClientAdapter -- only difference is encoding='utf-8'; add encoding to parent and delete overrides
  • [python-architect] import json as _json inside windsurf cleanup block in mcp_integrator.py -- json already imported at module level; remove redundant alias
  • [python-architect] windsurf_workflow format_id falls through to generic copy_instruction with no comment -- add a comment documenting this is intentional for plain markdown workflow files
  • [cli-logging-expert] No verbose-mode diagnostics emitted by configure_mcp_server -- resolved config_path and config_key should be logged via _rich_dim under --verbose
  • [devx-ux-expert] Consider adding a windsurf example to the compile --target section in cli-commands.md alongside the existing vscode example
  • [supply-chain-security-expert] Silent IOError swallow in get_current_config is fail-open -- distinguish FileNotFoundError (safe to ignore) from permission errors (should surface)
  • [supply-chain-security-expert] Same description-injection pattern exists in _convert_to_cursor_rules (pre-existing) -- worth fixing alongside the new windsurf instance
  • [oss-growth-hacker] Auto-detection directory list in how-it-works.md does not mention .windsurf/ -- low urgency prose inconsistency

CEO arbitration

The panel reached broad consensus on the critical shape of this PR: it is structurally incomplete. Five of five active panelists raised required findings, and three overlapping clusters crystallize the verdict. First, the hooks gap is the most architecturally dangerous: python-architect and devx-ux-expert independently identified that windsurf TargetProfile declares a hooks primitive but 'windsurf' is absent from _MERGE_HOOK_TARGETS, meaning hooks silently deploy zero files. This is not a nit -- silent feature failures are harder for users to diagnose than hard errors, and shipping a hooks surface that does nothing corrodes trust in the entire APM integration model. Second, the configure_mcp_server copy-paste is flagged as required by both python-architect (maintenance split) and cli-logging-expert (raw print() bypassing the rich output system). These are distinct but compounding harms on the same code block; both reviewers are correct, and both must be resolved before merge. The cli-logging-expert finding is not a subset of the python-architect finding -- fixing the duplication alone would still leave the print() regression in whichever copy survives. Third, supply-chain-security-expert raised three required findings (frontmatter injection via apply_to, YAML key injection via name/description, and missing ensure_path_within on instruction target_path). None of these have compensating controls elsewhere in the PR. Security findings at this severity are never overridden without a written trade-off and a follow-up issue; no such statement exists here. The documentation and discovery gaps (cli-commands.md, README, CHANGELOG, manifest-schema.md, apm-guide skill) raised by devx-ux-expert and oss-growth-hacker are required, not advisory, because APM's contract with contributors is that a feature does not exist until it is discoverable. A target absent from cli-commands.md and manifest-schema.md is effectively a hidden flag -- worse than no feature at all because it will generate confused support requests. The get_target_description mismatch (.windsurf/rules/ vs .windsurf/skills/ for agents) is a user-visible lie and must be corrected in the same PR that introduced it.

Dissent resolved: No panelist disagreed on required vs nit classification for any shared finding. The configure_mcp_server finding is raised independently by python-architect (maintenance split) and cli-logging-expert (print() regression). These are additive, not overlapping: the fix must both eliminate the duplication via super() delegation or a protected label property AND replace the surviving print() calls with the rich output helpers.

Growth/positioning note: Windsurf / Codeium is the fastest-growing AI IDE segment outside VS Code. Adding Windsurf as a compile target is a top-tier acquisition surface -- Windsurf users searching for AI package management will land on APM's README and see themselves reflected for the first time. When this PR is re-submitted with all required findings resolved, the CHANGELOG entry should be written as a headline feature ('APM now compiles and integrates to Windsurf'), not buried as an adapter addition -- this is positioning language, not just release hygiene.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class BaseMCPClientAdapter {
        <<Abstract>>
        +get_config_path() str
        +update_config(config_updates) None
        +get_current_config() dict
        +configure_mcp_server(...) bool
    }

    class CopilotClientAdapter {
        <<ConcreteAdapter>>
        +registry_client SimpleRegistryClient
        +get_config_path() str
        +update_config(config_updates) None
        +get_current_config() dict
        +configure_mcp_server(...) bool
        +_format_server_config(...) dict
    }

    class WindsurfClientAdapter {
        <<ConcreteAdapter>>
        +supports_user_scope bool
        +get_config_path() str
        +update_config(config_updates) None
        +get_current_config() dict
        +configure_mcp_server(...) bool
    }

    class HookIntegrator {
        <<BaseIntegrator>>
        +integrate_hooks_for_target(target) HookIntegrationResult
        +_integrate_merged_hooks(config) HookIntegrationResult
    }

    class _MergeHookConfig {
        <<ValueObject>>
        +config_filename str
        +target_key str
        +require_dir bool
    }

    class InstructionIntegrator {
        <<BaseIntegrator>>
        +copy_instruction_windsurf(...) int
        +_convert_to_windsurf_rules(content) str
    }

    class AgentIntegrator {
        <<BaseIntegrator>>
        +_write_windsurf_agent_skill(src, dest) None
    }

    BaseMCPClientAdapter <|-- CopilotClientAdapter
    CopilotClientAdapter <|-- WindsurfClientAdapter
    HookIntegrator ..> _MergeHookConfig : dispatches via _MERGE_HOOK_TARGETS
    note for WindsurfClientAdapter "configure_mcp_server body duplicates parent verbatim. Docstring says delegates to parent but does not call super(). windsurf absent from _MERGE_HOOK_TARGETS -> silent 0-files result."
    note for HookIntegrator "_MERGE_HOOK_TARGETS keys: claude | cursor | codex | gemini. windsurf MISSING -> silent no-op"

    class WindsurfClientAdapter:::touched
    class HookIntegrator:::touched
    class InstructionIntegrator:::touched
    class AgentIntegrator:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm compile --target windsurf]) --> B[factory.py: resolve WindsurfTarget]
    B --> C[agents_compiler.py: compile agents loop]
    B --> D[instruction_integrator.py: integrate instructions]
    B --> E[hook_integrator.py: integrate_hooks_for_target windsurf]

    C --> C1["[FS] AgentIntegrator._write_windsurf_agent_skill()\nf-string name/description -- injection risk\n-> skills/name/SKILL.md"]

    D --> D1{format_id?}
    D1 -->|windsurf_rules| D2["[FS] _convert_to_windsurf_rules()\napply_to inserted verbatim -- injection risk"]
    D1 -->|windsurf_workflow commands| D3["[FS] copy_instruction()\nplain copy -- falls through, no explicit branch"]
    D1 -->|windsurf_agent_skill| C1

    E --> E1{target.name in _MERGE_HOOK_TARGETS?}
    E1 -->|NO windsurf absent| E2["returns files_integrated=0\nno warning, no error -- SILENT FAILURE"]
    E1 -->|YES claude/cursor/codex/gemini| E3["[FS] _integrate_merged_hooks()"]
Loading

Design patterns

  • Used in this PR: Template Method (Base + subclass) -- WindsurfClientAdapter subclasses CopilotClientAdapter to inherit config-format logic and override only get_config_path(). Intended pattern is correct; execution is undermined by the verbatim configure_mcp_server duplication.
  • Pragmatic suggestion: Protected label property -- add _client_label: str = 'Copilot CLI' to CopilotClientAdapter and reference it in configure_mcp_server. WindsurfClientAdapter overrides only _client_label = 'Windsurf'. Zero body duplication, one-line override.

Required:

  1. configure_mcp_server duplicated verbatim (windsurf.py:87)
  2. windsurf_hooks silent no-op (hook_integrator.py:153)

Nits:

  1. update_config/get_current_config duplicated
  2. import json as _json redundant in mcp_integrator.py
  3. windsurf_workflow fallthrough undocumented

CLI Logging Expert

Required:

  1. configure_mcp_server uses raw print() at windsurf.py:97 for all four output paths -- bypasses rich* helpers and STATUS_SYMBOLS, breaks quiet/verbose mode.

Nits:

  1. No verbose-mode diagnostics for resolved config_path and config_key.

DevX UX Expert

Required:

  1. windsurf absent from cli-commands.md (4 enumeration sites at lines 94, 587, 1005, 1667)
  2. apm-guide commands.md has zero mentions of windsurf
  3. Hooks silently not deployed for windsurf
  4. get_target_description describes agents landing in .windsurf/rules/ but they land in .windsurf/skills/

Nits:

  1. Add windsurf example to compile --target section alongside vscode example.

Supply Chain Security Expert

Required:

  1. Frontmatter injection via apply_to in _convert_to_windsurf_rules (instruction_integrator.py:362)
  2. YAML key injection via name/description in _write_windsurf_agent_skill (agent_integrator.py:251)
  3. Missing ensure_path_within on instruction target_path (instruction_integrator.py:111)

Nits:

  1. Silent IOError swallow in get_current_config is fail-open.
  2. Same description-injection pattern in _convert_to_cursor_rules (pre-existing).

Auth Expert

Inactive -- No auth files changed; PR adds Windsurf IDE integration target via file-system paths only, inheriting auth from CopilotClientAdapter without modifying AuthResolver, token precedence, or host classification.

OSS Growth Hacker

Required:

  1. No CHANGELOG.md entry for windsurf support.
  2. README.md omits windsurf in three places (lines 7, 62, 140).
  3. manifest-schema.md target allowed-values table omits windsurf.

Nits:

  1. how-it-works.md auto-detection directory list omits .windsurf/.

Verdict computed deterministically: 13 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

  • #1066 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • Feature/windsurf target #1066 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1066 · ● 5.7M ·

@github-actions github-actions Bot added panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 30, 2026
@yoelabril yoelabril force-pushed the feature/windsurf-target branch 3 times, most recently from 274379a to 5bc180d Compare April 30, 2026 14:29
@yoelabril yoelabril requested a review from Copilot April 30, 2026 14:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Comment thread docs/src/content/docs/reference/manifest-schema.md
Comment thread README.md
Comment thread README.md
Comment thread src/apm_cli/integration/agent_integrator.py Outdated
Comment thread src/apm_cli/adapters/client/windsurf.py Outdated
Comment thread docs/src/content/docs/guides/compilation.md Outdated
Comment thread docs/src/content/docs/reference/cli-commands.md Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

src/apm_cli/integration/mcp_integrator.py:1033

  • MCP install-time runtime selection filters installed runtimes by those referenced in apm.yml scripts (via _detect_runtimes). Windsurf was added to the installed-runtime list, but _detect_runtimes() currently cannot detect windsurf commands, so a repo that references Windsurf in scripts could incorrectly skip configuring it. Add windsurf detection (and ensure any fallback allowlists include it) so script-based targeting works consistently.
                for runtime_name in ["copilot", "codex", "vscode", "cursor", "opencode", "gemini", "windsurf"]:
                    try:
                        if runtime_name == "vscode":
                            if _is_vscode_available(project_root=project_root_path):
                                ClientFactory.create_client(runtime_name)
                                installed_runtimes.append(runtime_name)
                        elif runtime_name == "cursor":
                            # Cursor is opt-in: only target when .cursor/ exists
                            if (project_root_path / ".cursor").is_dir():
                                ClientFactory.create_client(runtime_name)
                                installed_runtimes.append(runtime_name)
                        elif runtime_name == "opencode":
                            # OpenCode is opt-in: only target when .opencode/ exists
                            if (project_root_path / ".opencode").is_dir():
                                ClientFactory.create_client(runtime_name)
                                installed_runtimes.append(runtime_name)
                        elif runtime_name == "gemini":
                            # Gemini CLI is opt-in: only target when .gemini/ exists
                            if (Path.cwd() / ".gemini").is_dir():
                                ClientFactory.create_client(runtime_name)
                                installed_runtimes.append(runtime_name)
                        elif runtime_name == "windsurf":
                            # Windsurf is opt-in: only target when .windsurf/ exists
                            if (project_root_path / ".windsurf").is_dir():
                                ClientFactory.create_client(runtime_name)
                                installed_runtimes.append(runtime_name)
                        else:  # noqa: PLR5501
                            if manager.is_runtime_available(runtime_name):
                                ClientFactory.create_client(runtime_name)
                                installed_runtimes.append(runtime_name)
                    except (ValueError, ImportError):
                        continue
            except ImportError:
                installed_runtimes = [
                    rt for rt in ["copilot", "codex"] if shutil.which(rt) is not None
                ]
                # VS Code: check binary on PATH or .vscode/ directory presence
                if _is_vscode_available(project_root=project_root_path):
                    installed_runtimes.append("vscode")
                # Cursor is directory-presence based, not binary-based
                if (project_root_path / ".cursor").is_dir():
                    installed_runtimes.append("cursor")
                # OpenCode is directory-presence based
                if (project_root_path / ".opencode").is_dir():
                    installed_runtimes.append("opencode")
                # Gemini CLI is directory-presence based
                if (Path.cwd() / ".gemini").is_dir():
                    installed_runtimes.append("gemini")
                # Windsurf is directory-presence based
                if (project_root_path / ".windsurf").is_dir():
                    installed_runtimes.append("windsurf")

            # Step 2: Get runtimes referenced in apm.yml scripts
            script_runtimes = MCPIntegrator._detect_runtimes(
                apm_config.get("scripts", {}) if apm_config else {}
            )

Comment thread src/apm_cli/integration/instruction_integrator.py
Comment thread tests/unit/integration/test_instruction_integrator.py
Comment thread tests/unit/integration/test_scope_integration.py Outdated
Comment thread README.md
@yoelabril yoelabril force-pushed the feature/windsurf-target branch from 5bd22b7 to 5904c15 Compare April 30, 2026 21:12
Add Windsurf/Cascade support to APM's multi-target architecture.

Primitive mapping:
- instructions -> .windsurf/rules/*.md (trigger: glob)
- agents -> .windsurf/skills/<name>/SKILL.md (no native agent primitive)
- skills -> .windsurf/skills/<name>/SKILL.md (native format)
- prompts/commands -> .windsurf/workflows/*.md
- hooks -> .windsurf/hooks.json
- compiled instructions -> AGENTS.md (always-on, native support)

Implementation:
- Add TargetProfile to KNOWN_TARGETS (rules, skills, workflows, hooks)
- Create WindsurfClientAdapter for MCP config (~/.codeium/windsurf/mcp_config.json)
- Register in ClientFactory, target detection, agents compiler
- Add runtime detection and stale MCP cleanup in mcp_integrator
- Add windsurf_rules format handler (applyTo -> trigger/globs frontmatter)
- Add windsurf_agent_skill transformer (agent.md -> SKILL.md)
- AGENTS.md compilation enabled (Windsurf reads it natively)
- Tests: detection, scope, instruction conversion, agent skill conversionfeat: add windsurf/cascade as compilation and integration target

- Add TargetProfile to KNOWN_TARGETS (rules, skills, workflows, hooks)
- Create WindsurfClientAdapter for MCP config (~/.codeium/windsurf/mcp_config.json)
- Register in ClientFactory, target detection, agents compiler
- Add runtime detection and stale MCP cleanup in mcp_integrator
- Add windsurf_rules format handler (applyTo -> trigger/globs frontmatter)
- AGENTS.md compilation enabled (Windsurf reads it natively)
- Add tests: detection, scope, instruction conversion, integration (all pass)
@yoelabril yoelabril force-pushed the feature/windsurf-target branch from 5904c15 to 7f49689 Compare April 30, 2026 21:26
@yoelabril yoelabril requested a review from Copilot April 30, 2026 21:27
@danielmeppiel danielmeppiel added panel-review Trigger the apm-review-panel gh-aw workflow and removed panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. labels May 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

APM Review Panel: ship_with_followups

feat(windsurf): adds Windsurf/Cascade as a first-class APM target -- clean integration, two blocking doc gaps, one security gap in hook path guard, and strong growth upside if the narrative is sharpened.

cc @yoelabril -- a fresh advisory pass is ready for your review.

The panel converges on a clear verdict: the core Windsurf integration is architecturally sound and the path-boundary change from project_root to deploy_dir is correctly scoped (supply-chain-security and auth experts both confirm no traversal risk). The WindsurfClientAdapter inheritance model is clean. No panelist found a blocking code defect in the adapter, integrator, or compilation path.

Two findings warrant maintainer attention before or immediately after merge. First, doc-writer flags two blocking gaps: ide-tool-integration.md has no Windsurf section, and auto_create=False behavior is entirely undocumented -- users will run apm install and see no Windsurf output with no explanation. Every other first-class target has a dedicated integration guide section; parity here is a baseline user trust commitment. Second, supply-chain-security surfaces a real functional gap: hook script copy destinations in hook_integrator.py are not validated through ensure_path_within before shutil.copy2. For Windsurf user-scope this could silently deploy hooks to the wrong location (project-relative instead of user-home-relative). The test-coverage expert confirms there is zero test coverage for the Windsurf hook routing branch (outcome: missing), which under the panel's evidence-weighting rules elevates this to near-blocking tier. The python-architect's frontmatter parsing inconsistency (yaml.safe_load vs hand-rolled loop) is a real maintenance trap and should be fixed, but it is not blocking.

The OSS growth signal is concrete and non-trivial: Windsurf/Codeium's developer base is exactly APM's target persona, and native .windsurf/ deployment is a genuine differentiator with a narrow first-mover window. The README callout gap and the stale commands.md skill are low-effort, high-leverage fixes. The devx-ux warning about silently dropped tools/model frontmatter is important for user trust -- it should emit a per-file warning at install time so users understand the lossy mapping.

Dissent. No direct cross-panelist disagreement. The only tension is between devx-ux-expert and doc-writer both flagging the lossy agent->skill conversion: devx wants a runtime warning, doc-writer wants documentation. Both are right and non-exclusive.

Aligned with: Portability by manifest, Secure by default, Multi-harness support, OSS

Growth signal. Windsurf/Codeium's user base skews toward developers frustrated with heavyweight VS Code extensions -- exactly APM's adoption-ready persona. Native .windsurf/ deploy (rules, skills, workflows, hooks, MCP) is a differentiated story that no other package manager ships today. Adding a parallel Windsurf callout to the README (apm install # deploys to .windsurf/ automatically) and updating the commands.md skill are low-effort changes that compound: AI assistants using the APM guide skill will give correct Windsurf onboarding answers, creating a self-reinforcing loop. The first-mover claim is worth stating plainly in the CHANGELOG.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 2 Clean, well-structured target addition; two focused issues: inconsistent frontmatter parsing in _convert_to_windsurf_rules vs _write_windsurf_agent_skill.
CLI Logging Expert 0 1 1 Windsurf adapter is clean and consistent; inherits a pre-existing print() anti-pattern but introduces no new output regressions.
DevX UX Expert 0 2 2 Windsurf integration is solid; two gaps break the mental-model contract -- lossy agent->skill conversion is invisible, and apm-guide commands.md is stale.
Supply Chain Security Expert 0 1 2 Path-traversal change is architecturally correct and safe; hook script copy destinations lack ensure_path_within guard.
OSS Growth Hacker 0 2 2 Windsurf is technically wired up but the growth story is undertold -- README has no runnable example, CHANGELOG is engineer-written.
Auth Expert 0 0 2 WindsurfClientAdapter safely inherits Copilot auth logic; path relaxation introduces no auth bypass.
Doc Writer 2 5 0 Windsurf docs are partially complete -- integration guide missing Windsurf section, agent->skill lossy-mapping undocumented, auto_create=False unmentioned.
Test Coverage Expert 0 2 0 Hook-integrator windsurf routing and WindsurfClientAdapter.get_config_path() have no tests; all other key surfaces are covered.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Doc Writer] (blocking-severity) Add a Windsurf section to ide-tool-integration.md covering setup, auto_create=False behavior, user-scope limitations, and the lossy agent->skill mapping. -- Blocking doc gap: every other first-class target has an integration guide section. Without it, users see silent no-ops on apm install with no path to resolution.
  2. [Supply Chain Security Expert] Add ensure_path_within guard on hook script copy destinations in hook_integrator.py before shutil.copy2. -- For Windsurf user-scope, unguarded target_script resolves project-relative instead of user-home-relative -- both a functional mismatch and a missing path safety net. Test-coverage-expert confirms zero test coverage for this branch (outcome: missing), elevating risk.
  3. [Test Coverage Expert] Add unit tests for WindsurfClientAdapter.get_config_path() and the Windsurf hook routing branch in test_hook_integrator.py. -- Both are outcome: missing on surfaces that touch path resolution and file deployment -- the two highest regression-trap areas in this PR.
  4. [DevX UX Expert] Emit a per-file warning in _write_windsurf_agent_skill when tools or model frontmatter keys are dropped during agent->skill conversion. -- Silent data loss at install time breaks the 'install adds, never silently mutates' mental model.
  5. [Python Architect] Replace the hand-rolled applyTo: line-scan in _convert_to_windsurf_rules with yaml.safe_load on the frontmatter block. -- Every other frontmatter parser in the codebase uses yaml.safe_load; the inconsistency silently misparses quoted values and is a maintenance trap.

Architecture

classDiagram
    direction LR

    class MCPClientAdapter {
        <<ABC>>
        +get_config_path() str
        +update_config(updates) None
        +supports_user_scope bool
    }

    class CopilotClientAdapter {
        <<ConcreteAdapter>>
        +_client_label str
        +get_config_path() str
        +update_config(updates) None
    }

    class WindsurfClientAdapter {
        <<ConcreteAdapter>>
        +_client_label str
        +supports_user_scope bool
        +get_config_path() str
    }

    class BaseIntegrator {
        <<BaseClass>>
        +check_collision() bool
        +resolve_links() tuple
        +ensure_path_within() Path
    }

    class InstructionIntegrator {
        <<ConcreteIntegrator>>
        +integrate_instructions_for_target(target, ...) IntegrationResult
        +copy_instruction_windsurf(src, tgt) int
        +_convert_to_windsurf_rules(content) str
    }

    class AgentIntegrator {
        <<ConcreteIntegrator>>
        +integrate_agents_for_target(target, ...) IntegrationResult
        +_write_windsurf_agent_skill(src, tgt) int
    }

    class HookIntegrator {
        <<ConcreteIntegrator>>
        +integrate_hooks_for_target(target, ...) IntegrationResult
    }

    class TargetProfile {
        <<ValueObject>>
        +name str
        +root_dir str
        +primitives dict
        +user_supported str
    }

    MCPClientAdapter <|-- CopilotClientAdapter : extends
    CopilotClientAdapter <|-- WindsurfClientAdapter : extends
    BaseIntegrator <|-- InstructionIntegrator : extends
    BaseIntegrator <|-- AgentIntegrator : extends
    BaseIntegrator <|-- HookIntegrator : extends
    TargetProfile *-- PrimitiveMapping : contains
    InstructionIntegrator ..> TargetProfile : reads format_id
    AgentIntegrator ..> TargetProfile : reads format_id
    HookIntegrator ..> TargetProfile : reads format_id

    note for WindsurfClientAdapter "Template Method: inherits mcpServers JSON logic, overrides get_config_path()"
    note for InstructionIntegrator "Strategy dispatch on format_id: cursor_rules / claude_rules / windsurf_rules"

    class WindsurfClientAdapter:::touched
    class InstructionIntegrator:::touched
    class AgentIntegrator:::touched
    class HookIntegrator:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A(["apm install --target windsurf"]) --> B["dispatch.py\nintegrate_for_target(windsurf)"]
    B --> C["InstructionIntegrator\nintegrate_instructions_for_target()"]
    B --> D["AgentIntegrator\nintegrate_agents_for_target()"]
    B --> E["HookIntegrator\nintegrate_hooks_for_target()"]
    B --> F["MCPIntegrator\nWindsurfClientAdapter.update_config()"]

    C --> C1["fmt == windsurf_rules?"]
    C1 -- yes --> C2["ensure_path_within(target_path, deploy_dir)"]
    C2 --> C3["_convert_to_windsurf_rules(content)"]
    C3 --> C4["applyTo present?"]
    C4 -- yes --> C5["trigger: glob + globs"]
    C4 -- no --> C6["trigger: always_on"]
    C5 --> C7[".windsurf/rules/name.md"]
    C6 --> C7

    D --> D1["_write_windsurf_agent_skill(src, tgt)"]
    D1 --> D2["yaml.safe_load frontmatter\nstrip model/tools keys"]
    D2 --> D3[".windsurf/skills/name/SKILL.md"]

    E --> E1["windsurf branch: base_root=.windsurf"]
    E1 --> E2[".windsurf/hooks/pkg/ [NO path guard]"]

    F --> F1["get_config_path()\n~/.codeium/windsurf/mcp_config.json"]
    F1 --> F2["merge mcpServers\nwrite mcp_config.json"]
Loading

Recommendation

The integration is structurally sound and the first-class Windsurf target is a genuine APM milestone worth shipping. The two doc-writer blocking findings (ide-tool-integration.md Windsurf section + auto_create=False explanation) should be resolved in-PR or in an immediate follow-on before the release is announced -- users hitting a silent no-op with no docs is a trust-eroding first impression. The hook path guard (supply-chain-security) and missing tests (test-coverage-expert) should be tracked as follow-up issues and closed before the next minor release. The growth story -- README callout, CHANGELOG first-mover framing, commands.md skill update -- is low-effort and high-leverage; fold it into the same doc pass.


Full per-persona findings

Python Architect

  • [recommended] _convert_to_windsurf_rules parses applyTo: with a line-scan instead of yaml.safe_load at src/apm_cli/integration/instruction_integrator.py:357
    Every other frontmatter parser in this codebase uses yaml.safe_load. The hand-rolled loop silently misparses quoted values, multi-line block scalars, and YAML anchors. The inconsistency creates a maintenance trap: if applyTo semantics expand, one path is updated and the other is forgotten.
    Suggested: Replace the for-loop with yaml.safe_load on the fm_block, then read apply_to = fm.get('applyTo', ''), mirroring the pattern in _write_windsurf_agent_skill.

  • [nit] _write_windsurf_agent_skill should be @staticmethod like _write_codex_agent or have a comment explaining why it needs self at src/apm_cli/integration/agent_integrator.py:280
    The sibling _write_codex_agent is @staticmethod. Add a brief comment: # instance method: requires self.resolve_links().

  • [nit] Lazy import of ensure_path_within inside method body is inconsistent with module-level import style at src/apm_cli/integration/instruction_integrator.py:106
    Same PR moves import yaml to module level. Move from apm_cli.utils.path_security import ensure_path_within to module-level import block too.

CLI Logging Expert

  • [recommended] Success/error messages in inherited configure() path use raw print() instead of _rich_success/_rich_error at src/apm_cli/adapters/client/copilot.py
    PR expands the surface of a pre-existing anti-pattern. Adding a Windsurf adapter that inherits raw print() calls makes both targets inconsistent with the rest of the codebase's rich output.
    Suggested: Replace print(f"Successfully configured...") with _rich_success(...) in CopilotClientAdapter.configure(). Windsurf inherits the fix for free.

  • [nit] Stale-removal log hardcodes 'Windsurf config' rather than using _client_label at src/apm_cli/integration/mcp_integrator.py
    Low priority; tracked for when the stale-removal block is refactored to drive strings from the adapter.

DevX UX Expert

  • [recommended] Agent->skill conversion silently drops tools and model frontmatter with no user-visible warning at src/apm_cli/integration/agent_integrator.py
    Violates the 'install adds, never silently mutates' mental model. Users debugging why Windsurf skill tool boundaries don't work have no signal from APM.
    Suggested: In _write_windsurf_agent_skill, detect whether tools or model keys were present and emit a per-file note in the install summary.

  • [recommended] packages/apm-guide/.apm/skills/apm-usage/commands.md not updated -- windsurf missing from apm runtime setup target list at packages/apm-guide/.apm/skills/apm-usage/commands.md
    AI assistants using this skill to answer Windsurf setup questions will give incomplete guidance.

  • [nit] get_target_description('windsurf') omits .windsurf/hooks.json from dry-run output at src/apm_cli/core/target_detection.py
    Add + .windsurf/hooks.json to the description string to match other targets with hooks.

  • [nit] compilation.md auto-detection table label AGENTS.md (instructions only) is misleading for Windsurf at docs/src/content/docs/guides/compilation.md
    Change to AGENTS.md (optional roll-up; install deploys to .windsurf/rules/) or simply AGENTS.md.

Supply Chain Security Expert

  • [recommended] Hook script copy destinations (project_root / target_rel) are not passed through ensure_path_within before shutil.copy2 at src/apm_cli/integration/hook_integrator.py
    Verified: target_script at lines 599 and 779 of hook_integrator.py is never checked via ensure_path_within. For Windsurf user-scope, base_root set to .codeium/windsurf would make target_script resolve project-relative instead of user-home-relative -- a functional mismatch and missing path safety net.
    Suggested: Wrap each shutil.copy2 site with ensure_path_within(target_script, project_root).

  • [nit] ensure_path_within change from project_root to deploy_dir is correct but warrants an inline proof comment at src/apm_cli/integration/instruction_integrator.py
    Add: # target_name is Path.name (no separators), so traversal via deploy_dir is impossible.

  • [nit] MCP env block written verbatim to global ~/.codeium/windsurf/mcp_config.json -- inherited risk, no new exposure at src/apm_cli/adapters/client/windsurf.py
    Pre-existing shared behavior across all adapters. Tracked for future allowlist consideration.

OSS Growth Hacker

  • [recommended] README has zero runnable Windsurf example -- Windsurf users have no 'aha' moment at README.md
    Copilot has a zero-config callout with a single command. Windsurf appears only as a name in the IDE list. Top-of-funnel conversion miss.
    Suggested: Add apm install # deploys to .windsurf/rules/, .windsurf/skills/, .windsurf/workflows/ automatically after the Copilot example.

  • [recommended] compilation.md auto-detection table says AGENTS.md (instructions only) -- actively undersells the full native deploy at docs/src/content/docs/guides/compilation.md
    A Windsurf user reading this table may conclude APM support is thin and bounce.

  • [nit] CHANGELOG entry is written for maintainers, not users -- the hook is absent at CHANGELOG.md
    Prepend user hook: 'Windsurf/Cascade users can now apm install and get a fully configured IDE automatically.'

  • [nit] First-mover claim is never stated in any shipped doc despite a narrow window
    Add (the first package manager with native Windsurf support) to the CHANGELOG entry or README callout.

Auth Expert

  • [nit] GitHub token injection inherited by WindsurfClientAdapter -- same threat model as Copilot, no new exposure at src/apm_cli/adapters/client/windsurf.py
    Fires only for GitHub MCP servers (URL/name heuristic). File permissions are OS-level user-owned. No new credential exposure surface.

  • [nit] Path boundary relaxation from project_root to deploy_dir is correct -- cannot escape to ~/.ssh or ~/.gitconfig at src/apm_cli/integration/instruction_integrator.py:118
    deploy_dir is target_root/mapping.subdir; cannot traverse above the deploy target. Semantics are correct.

Doc Writer

  • [blocking] No Windsurf section in ide-tool-integration.md -- every other first-class target has one at docs/src/content/docs/integrations/ide-tool-integration.md
    Add a ### Windsurf subsection mirroring the Cursor section: primitive-mapping table, auto_create=False setup note, agent->skill conversion note, and partial user-scope caveat.

  • [blocking] auto_create=False undocumented -- users will run apm install and see no Windsurf output with no explanation at docs/src/content/docs/integrations/ide-tool-integration.md
    Add: 'Create a .windsurf/ directory in your project root (or open the project in Windsurf once), then run apm install.'

  • [recommended] Agent->skill conversion (lossy: tools/model dropped) not documented at docs/src/content/docs/integrations/ide-tool-integration.md
    Add a note row in the Windsurf primitive-mapping table: 'Agents deploy as Windsurf Skills; tools and model frontmatter are not preserved.'

  • [recommended] Partial user-scope limitation (instructions excluded) not disclosed in any doc at docs/src/content/docs/integrations/ide-tool-integration.md
    Note: 'User-scope deployment is partial -- instructions are not deployed globally (Windsurf global rules use a single-file format incompatible with APM).'

  • [recommended] compilation.md auto-detection table misleading for Windsurf (AGENTS.md (instructions only)) at docs/src/content/docs/guides/compilation.md
    Clarify or add a footnote that the table covers apm compile output, not apm install.

  • [recommended] CHANGELOG Windsurf entry omits that user-scope is partial at CHANGELOG.md
    Amend: 'user-scope deployment (partial: instructions excluded at user scope -- global rules use a different Windsurf format).'

  • [recommended] compilation.md intro says 'all primitives natively' -- masks agent->skill conversion at docs/src/content/docs/guides/compilation.md
    Qualify: 'agents as Windsurf Skills to .windsurf/skills/' (or link to integration guide note).

Test Coverage Expert

  • [recommended] WindsurfClientAdapter.get_config_path() has no unit test at src/apm_cli/adapters/client/windsurf.py
    Proof (missing at): tests/unit/test_windsurf_adapter.py -- proves: apm mcp add --target windsurf writes to the correct global config file [multi-harness-support, devx]

  • [recommended] hook_integrator.py windsurf routing branch has no test at src/apm_cli/integration/hook_integrator.py
    Proof (missing at): tests/unit/integration/test_hook_integrator.py -- proves: apm install deploys hook scripts to .windsurf/hooks/<pkg>/ when windsurf is the target [multi-harness-support, devx]

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

  • #1066 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • Feature/windsurf target #1066 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1066 · ● 7M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label May 2, 2026
@yoelabril yoelabril force-pushed the feature/windsurf-target branch 2 times, most recently from b863385 to 01b3245 Compare May 2, 2026 14:53
@yoelabril yoelabril force-pushed the feature/windsurf-target branch from 01b3245 to f7cba3f Compare May 2, 2026 15:03
jaloncad and others added 7 commits May 2, 2026 17:10
Per devx-ux panel review on PR microsoft#1066: _write_windsurf_agent_skill
silently strips 'tools' and 'model' fields when converting an .agent.md
to a Windsurf SKILL.md, because Windsurf Skills do not support
agent-only metadata. Silent loss is a footgun.

Thread an optional diagnostics parameter through the method and emit
a DiagnosticCollector.warn() entry per source file when either field
is present. Message names the dropped field(s) and the source filename
so the install summary surfaces the loss after all packages process.

The DiagnosticCollector is the canonical collect-then-render pattern
in this repo (utils/diagnostics.py); the renderer prepends [!] via
_rich_warning, so we do not echo inline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per supply-chain panel review on PR microsoft#1066: the two shutil.copy2 sites
in hook_integrator.py constructed target_script from package-controlled
JSON (the rewritten hook command), without asserting the resolved path
stays inside project_root. A malicious package could craft a target_rel
that escapes via .. traversal, and mkdir(parents=True) would happily
materialize attacker-controlled directories outside the workspace
before the copy ran.

Wrap both copy loops with ensure_path_within(target_script, project_root)
BEFORE mkdir, matching the canonical pattern in instruction_integrator
and skill_integrator. PathTraversalError propagates so the install
aborts on the first malicious entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per doc-writer panel review on PR microsoft#1066: ide-tool-integration.md had
zero coverage of the Windsurf target. Add a parallel section after
Claude Integration covering:

- Opt-in auto-detection (auto_create=False; .windsurf/ must exist)
- Native Windsurf primitives table (rules / skills / workflows / hooks)
- Lossy agent-to-skill conversion (tools and model dropped, with the
  runtime warning emitted by agent_integrator)
- MCP user-scope config at ~/.codeium/windsurf/mcp_config.json
- User-scope install limitations (instructions stay workspace-local
  per unsupported_user_primitives)

Every behavior claim cross-checked against
src/apm_cli/integration/targets.py, agent_integrator.py, and
adapters/client/windsurf.py. ASCII-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per test-coverage panel review on PR microsoft#1066: zero windsurf-specific
unit tests. Add 9 new test functions across 3 files.

tests/unit/test_windsurf_adapter.py (new):
- get_config_path() resolves to ~/.codeium/windsurf/mcp_config.json
- supports_user_scope is True; client label is Windsurf

tests/unit/integration/test_hook_integrator.py:
- Windsurf branch of _rewrite_hook_command rewrites
  CLAUDE_PLUGIN_ROOT references to .windsurf/hooks/<pkg>/...
- Relative path rewriting and bare system commands
- PathTraversalError raised when target_rel escapes project_root
  (regression test for the new ensure_path_within guard)

tests/unit/integration/test_agent_integrator.py:
- _write_windsurf_agent_skill records a DiagnosticCollector warning
  when frontmatter has tools or model
- No warning recorded when neither field is present

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator

Panel follow-ups addressed

Pushed 4 commits (76a1315f ... 751c11a8) addressing the remaining items from the synthesizer comment:

# Follow-up Status Commit
1 Doc gap: Windsurf section in ide-tool-integration.md done 521c0b44
2 Path-traversal guard on shutil.copy2 in hook_integrator.py (both sites) done 0beed8b3
3 Warning when _write_windsurf_agent_skill drops tools / model done 76a1315f
4 Test coverage: adapter, hook routing, path-traversal regression, dropped-fields diagnostic done (+9 tests) 751c11a8
5 yaml.safe_load in _convert_to_windsurf_rules already addressed by author ---

Verification

  • uv run --extra dev ruff check src/ tests/ -- silent
  • uv run --extra dev ruff format --check src/ tests/ -- silent on touched files
  • uv run --extra dev pytest tests/unit --ignore=tests/unit/test_audit_report.py -- 7254 passed (200 in the touched modules)

Implementation notes

  • Security guard (hook_integrator.py): ensure_path_within(target_script, project_root) runs before mkdir(parents=True) so a malicious target_rel cannot leave attacker-controlled directories behind even if the copy were later blocked. Mirrors the canonical pattern in instruction_integrator.py:118 and skill_integrator.py:374.
  • Dropped-field warning: routed through the canonical DiagnosticCollector (utils/diagnostics.py), not inline print/_rich_warning. The install summary renders [!] after all packages process. Message contains tools and/or model verbatim so future tests can assert on it.
  • Docs section: every claim cross-checked against targets.py, agent_integrator.py, adapters/client/windsurf.py. ASCII-only per repo encoding rules.
  • Tests: Windows-compatible (pathlib.Path joining only); the path-traversal test was initially @pytest.mark.skip waiting on the security guard and was unskipped once the guard landed.

Authored via the apm-review-panel specialist agents (doc-writer, supply-chain-security-expert, cli-logging-expert, test-coverage-expert).

Daniel Meppiel and others added 6 commits May 2, 2026 23:55
…tration

Adds 3 fields to TargetProfile (pack_prefixes, compile_family,
hooks_config_display) and 1 attribute to MCPClientAdapter
(mcp_servers_key) so adding a new target becomes a single registry
edit instead of touching 4 module-local dicts/if-elif chains.

Adds tests/unit/integration/test_targets_registry_completeness.py
(52 parametrized tests) which turns "forgot to wire up new target"
from a silent runtime bug into a CI failure.

Zero behavior change in this commit: consumers are migrated in
follow-ups (conflict_detector, lockfile_enrichment, compile/cli,
install/services).

Also reformats two pre-existing files (target_detection.py,
mcp_integrator.py) so the lint contract is silent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… silent breaks

Replaces classname-substring sniffing in get_existing_server_configs
with a direct lookup of self.adapter.mcp_servers_key.  This repairs
silent-no-conflict behavior for cursor, gemini, opencode, and windsurf
adapters (whose class names contained none of the hard-coded
substrings copilot/codex/vscode/claude, so the dispatch fell through
and returned {} -- conflict detection was a silent no-op).

The codex TOML flat-key fallback (mcp_servers."name") is preserved
under the same branch.

Updates 3 existing tests that patched __class__.__name__ to instead
set adapter.target_name and adapter.mcp_servers_key.  Adds 7 new
regression tests covering windsurf/cursor/gemini/opencode/vscode
parity plus codex flat-key merging and the empty-key defensive case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…+gemini drops

Replaces the static _TARGET_PREFIXES dict in bundle/lockfile_enrichment
with a _get_target_prefixes() helper that reads
TargetProfile.effective_pack_prefixes off the registry, plus an
_all_target_prefixes() helper that unions every deployable target.

Behavior fixes (incidental but desired):
- apm pack --target windsurf: .windsurf/ files were silently dropped
  (windsurf was missing from _TARGET_PREFIXES); now included.
- apm pack --target all: .windsurf/ AND .gemini/ files were silently
  dropped (both missing from the hard-coded "all" list); now included
  via deterministic union of detect_by_dir/auto_create profiles.

Adds windsurf entry to _CROSS_TARGET_MAPS so .github/skills/ remap
to .windsurf/skills/ and .github/agents/ collapse onto
.windsurf/skills/ (windsurf has no native agent surface).

Adds 6 regression tests pinning windsurf/all/cross-map behavior plus
a "every legacy target still filters correctly" guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ompile_family

Replaces hard-coded copilot_family / agents_md_family sets in
_resolve_compile_target with a registry-driven derivation that reads
TargetProfile.compile_family from KNOWN_TARGETS.  Adding a new
agents-family target (e.g. windsurf, zed, cline) becomes a single
field on the registry entry instead of touching this function.

Behavior fixes:
- -t windsurf and -t [windsurf]: previously fell through to the
  hard-coded 'cursor/opencode/codex' bare-fallback list; now routes
  through compile_family='agents' the same way cursor/codex/opencode do.
- -t [windsurf, claude] / -t [windsurf, gemini] / -t [windsurf, copilot]:
  previously collapsed to wrong family; now produces the expected
  frozenset / 'vscode' tokens.

Bare-target priority is preserved by iterating KNOWN_TARGETS in
insertion order, so -t [opencode, codex] still resolves to 'opencode'.

Adds a windsurf-specific test plus extends the existing bare-target
test to cover windsurf.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the if/elif chain on _target.name with a single
profile-driven lookup.  Fixes the silent log-message regression where
windsurf hook installs printed the deploy path as ".windsurf/" instead
of ".windsurf/hooks.json".  Adding any future target with hooks costs
zero edits to this code path -- just populate hooks_config_display on
the TargetProfile entry.

The existing registry-completeness guard already enforces that
hooks_config_display lives under the target's root_dir, so any
typo lands at test time rather than runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Locally-run apm-review-panel (python-architect, cli-logging-expert,
test-coverage-expert) flagged four residual gaps after the structural
refactor.  Address them in one cleanup pass:

1. Gemini hooks_config_display
   Gemini's TargetProfile omitted hooks_config_display, so install logs
   would print ".gemini/hooks/" instead of ".gemini/settings.json"
   (where Gemini hooks actually merge).  Same class of silent UX
   regression the PR already fixed for windsurf.  Add the field; the
   existing registry-completeness guard
   (test_hooks_display_matches_root) now covers it.

2. mcp_integrator runtime lists
   mcp_integrator.py:461 (cleanup loop) and :850 (availability fallback)
   each maintained a hard-coded list of MCP-capable runtimes that had
   to be updated per-target.  Extract _MCP_CLIENT_REGISTRY to a
   module-level constant in factory.py and expose
   ClientFactory.supported_clients(); both call sites now derive from
   it.  Adding a new MCP target is one edit (the registry dict)
   instead of three.

3. CLI --target help string drift
   pack.py:68, compile/cli.py:267, install.py:841 all enumerated valid
   target values without "windsurf".  Users discovering the flag via
   --help would not know windsurf is valid.  Adds windsurf to all three.

4. Structural guard for --target all
   The existing canary (test_target_all_includes_windsurf_files) pins
   today's behavior but a future deployable target could still be
   silently dropped.  Add test_target_all_includes_every_deployable_
   target_prefix in TestWindsurfTargetParity that iterates KNOWN_TARGETS
   and asserts every detect_by_dir/auto_create profile's prefixes
   appear in _all_target_prefixes().  Also add test_client_factory_
   supported_clients_matches_adapter_set in the registry-completeness
   guard so the factory dict cannot drift from the adapter subclass set.

Plus a stale-docstring fix in lockfile_enrichment.py:92 (referenced the
removed _TARGET_PREFIXES constant).

Validated: ruff lint+format silent, 7322 unit tests pass (+2 new
structural guards), integration sanity green
(test_pack_unified, test_pack_unpack_e2e, test_multi_runtime).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator

Structural refactor + advisory panel pass (6 commits)

Per the python-architect critique upthread, the windsurf parity gaps were symptoms of the same root cause: per-target metadata scattered across module-local dicts and if/elif chains. Rather than only patch each gap in place, this push centralizes the metadata onto TargetProfile + MCPClientAdapter and adds a structural guard so future targets cannot regress these subsystems silently.

Commits

  1. 0332c7d6 refactor(targets): centralize per-target metadata to remove N+1 registration
  2. a3783de3 fix(conflict-detector): dispatch on adapter.mcp_servers_key, repair 4 silent breaks
  3. fd63b1fb fix(pack): derive target prefixes from KNOWN_TARGETS, repair windsurf+gemini drops
  4. 2babc9d6 fix(compile): derive multi-target family routing from TargetProfile.compile_family
  5. 42f76e83 fix(install): use TargetProfile.hooks_config_display for hook log path
  6. f8aa150d fix(targets): close residual N+1 sites surfaced by review panel

Behavior fixes (silent regressions discovered + repaired)

  • apm pack --target windsurf now packs .windsurf/ files (was empty).
  • apm pack --target all now includes .windsurf/ AND .gemini/ files (the legacy hard-coded _TARGET_PREFIXES["all"] was missing both).
  • apm compile --target windsurf and --target [windsurf, *] now route via compile_family="agents" (was falling through to the wrong family).
  • MCP conflict detection for windsurf now returns the actual server map (was returning {} because dispatch sniffed class names).
  • Hook install log for windsurf now prints .windsurf/hooks.json (was .windsurf/).
  • Hook install log for gemini now prints .gemini/settings.json (was .gemini/hooks/).
  • CLI --target help in pack, compile, install now lists windsurf.

Structural guards (new)

tests/unit/integration/test_targets_registry_completeness.py (53 parametrized tests) -- adding a new target with missing pack_prefixes, compile_family, hooks_config_display, adapter.target_name, adapter.mcp_servers_key, or ClientFactory registration fails CI immediately rather than silently misbehaving at runtime.

test_target_all_includes_every_deployable_target_prefix -- iterates KNOWN_TARGETS and asserts every detect_by_dir/auto_create profile's prefixes appear in _all_target_prefixes(). Catches the general silent-drop class of bug, not just the windsurf canary.

Advisory panel results (run locally)

Persona Findings Disposition
python-architect 2 recommended, 2 nits All 2 recommended fixed (mcp_integrator runtime lists derived from ClientFactory.supported_clients(); help strings updated). 1 nit fixed (stale docstring). 1 nit deferred (cross-target maps guard, low risk).
cli-logging-expert 1 recommended, 2 nits Recommended fixed (gemini hooks_config_display). Nits deferred (verbose-mode diagnostics, docstring polish).
test-coverage-expert 1 recommended, 1 nit Recommended fixed (structural --target all guard). Nit deferred (integration-tier windsurf install+hook test -- structural guards judged sufficient).

Validation

  • uv run --extra dev ruff check src/ tests/ -- silent
  • uv run --extra dev ruff format --check src/ tests/ -- silent
  • uv run --extra dev pytest tests/unit --ignore=tests/unit/test_audit_report.py -- 7322 passed (+15 new tests across this push)
  • uv run --extra dev pytest tests/integration/test_pack_unified.py tests/integration/test_pack_unpack_e2e.py tests/integration/test_multi_runtime_integration.py -- 12 passed, 2 skipped

Adding a new target (e.g. zed, cline) is now: one entry in KNOWN_TARGETS, one adapter class with target_name + mcp_servers_key, one entry in _MCP_CLIENT_REGISTRY. The guard tests will tell you immediately if you forgot any subsystem.

@danielmeppiel
Copy link
Copy Markdown
Collaborator

Architecture diagrams (python-architect)

Per the apm-review-panel python-architect output, here are the structural diagrams for the post-refactor topology.

Class diagram -- single source of truth + adapter strategy

classDiagram
    direction LR

    class TargetProfile {
        <<Registry ValueObject>>
        +name str
        +root_dir str
        +pack_prefixes tuple
        +compile_family str or None
        +hooks_config_display str or None
        +effective_pack_prefixes tuple
    }

    class MCPClientAdapter {
        <<Strategy>>
        +target_name str
        +mcp_servers_key str
        +get_config_path()*
        +get_current_config()*
    }

    class CopilotClientAdapter {
        <<ConcreteStrategy>>
        +target_name = "copilot"
        +mcp_servers_key = "mcpServers"
    }

    class WindsurfClientAdapter {
        <<ConcreteStrategy>>
        +target_name = "windsurf"
        +mcp_servers_key = "mcpServers"
    }

    class VSCodeClientAdapter {
        <<ConcreteStrategy>>
        +target_name = "vscode"
        +mcp_servers_key = "servers"
    }

    class CodexClientAdapter {
        <<ConcreteStrategy>>
        +target_name = "codex"
        +mcp_servers_key = "mcp_servers"
    }

    class MCPConflictDetector {
        <<Consumer>>
        +get_existing_server_configs() dict
    }

    class KNOWN_TARGETS {
        <<Module-level Registry>>
        +dict~str, TargetProfile~
    }

    MCPClientAdapter <|-- CopilotClientAdapter
    CopilotClientAdapter <|-- WindsurfClientAdapter
    MCPClientAdapter <|-- VSCodeClientAdapter
    MCPClientAdapter <|-- CodexClientAdapter
    MCPConflictDetector o-- MCPClientAdapter : reads mcp_servers_key
    MCPConflictDetector ..> KNOWN_TARGETS : joins via target_name
    KNOWN_TARGETS *-- TargetProfile : contains

    note for KNOWN_TARGETS "Single source of truth for per-target metadata: pack_prefixes, compile_family, hooks_config_display"
    note for MCPClientAdapter "mcp_servers_key lives here (not on TargetProfile) because it describes config schema owned by the adapter, and applies to MCP-only targets (vscode) with no profile"

    class TargetProfile:::touched
    class MCPClientAdapter:::touched
    class CopilotClientAdapter:::touched
    class WindsurfClientAdapter:::touched
    class VSCodeClientAdapter:::touched
    class CodexClientAdapter:::touched
    class MCPConflictDetector:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading

Component diagram -- consumers reading from the registry

flowchart TD
    subgraph CLI_Entry["CLI entry points"]
        PACK["apm pack --target T"]
        COMPILE["apm compile --target T"]
        INSTALL["apm install"]
    end

    subgraph Registry["targets.py -- single source of truth"]
        KT["KNOWN_TARGETS dict<br/>9 TargetProfile entries"]
    end

    subgraph Pack["bundle/lockfile_enrichment.py"]
        GTP["_get_target_prefixes(target)"]
        ATP["_all_target_prefixes()"]
        FFT["_filter_files_by_target()"]
    end

    subgraph Compile["commands/compile/cli.py"]
        RCT["_resolve_compile_target(target)<br/>reads profile.compile_family"]
    end

    subgraph Conflict["core/conflict_detector.py"]
        GESC["get_existing_server_configs()<br/>reads adapter.mcp_servers_key"]
    end

    subgraph Install["install/services.py"]
        IPP["integrate_package_primitives()<br/>reads profile.hooks_config_display"]
    end

    subgraph Adapters["adapters/client/*.py"]
        ADP["MCPClientAdapter subclasses<br/>target_name + mcp_servers_key"]
    end

    PACK --> GTP
    GTP -->|"reads effective_pack_prefixes"| KT
    GTP --> ATP
    ATP -->|"iterates deployable profiles"| KT
    GTP --> FFT

    COMPILE --> RCT
    RCT -->|"reads compile_family"| KT

    INSTALL --> IPP
    IPP -->|"reads hooks_config_display"| KT
    INSTALL --> GESC
    GESC -->|"reads mcp_servers_key"| ADP
    ADP -->|"target_name joins to"| KT

    style KT fill:#fff3b0,stroke:#d47600
    style ADP fill:#fff3b0,stroke:#d47600
    style GTP fill:#fff3b0,stroke:#d47600
    style RCT fill:#fff3b0,stroke:#d47600
    style GESC fill:#fff3b0,stroke:#d47600
    style IPP fill:#fff3b0,stroke:#d47600
Loading

Sequence diagram -- adding a new target ("zed") in the post-refactor world

Illustrates the cost of onboarding a new agent harness now that metadata is centralized. Three edits + automatic structural-guard validation -- no scattered if/elif chains to discover.

sequenceDiagram
    autonumber
    actor Dev as Contributor
    participant Reg as integration/targets.py<br/>(KNOWN_TARGETS)
    participant Adp as adapters/client/zed.py<br/>(ZedClientAdapter)
    participant Fac as factory.py<br/>(_MCP_CLIENT_REGISTRY)
    participant Guard as test_targets_registry_completeness.py
    participant Pack as lockfile_enrichment._all_target_prefixes()
    participant Compile as compile/cli._resolve_compile_target()
    participant Install as install/services.integrate_package_primitives()
    participant Conflict as conflict_detector.get_existing_server_configs()

    Dev->>Reg: add TargetProfile("zed", root_dir=".zed",<br/>pack_prefixes=(".zed/",), compile_family="agents",<br/>hooks_config_display=".zed/hooks.json")
    Dev->>Adp: subclass MCPClientAdapter,<br/>target_name="zed", mcp_servers_key="mcpServers"
    Dev->>Fac: register "zed" -> ZedClientAdapter

    Note over Pack,Conflict: All consumers below pick up the new target<br/>without code changes -- they read from the registry.

    Pack-->>Reg: iterate KNOWN_TARGETS where<br/>detect_by_dir or auto_create
    Pack-->>Pack: --target all now includes ".zed/"
    Compile-->>Reg: read profile.compile_family for zed
    Compile-->>Compile: --target windsurf,zed routes to "agents" family
    Install-->>Reg: read profile.hooks_config_display
    Install-->>Install: log path = ".zed/hooks.json"
    Conflict-->>Adp: read adapter.mcp_servers_key
    Conflict-->>Conflict: extract servers for zed correctly

    Dev->>Guard: pytest tests/unit/integration/test_targets_registry_completeness.py
    alt Field missing on ZedClientAdapter or TargetProfile
        Guard-->>Dev: FAIL: <persona> drift detected, fix at registration time
    else All registry fields populated
        Guard-->>Dev: 53 passed -- new target wired up correctly
    end
Loading

The frontmatter serializer uses yaml.safe_dump to produce a string
(.rstrip on the return value), not to write to a file handle. The
CI yaml-io guard regex matches any kwarg after the first arg and
flagged this as a false positive. Wrap the call across lines and
add the # yaml-io-exempt marker to make the intent explicit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel merged commit 0826a84 into microsoft:main May 2, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status/accepted Direction approved, safe to start work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants