Skip to content

feat(cursor): add slash command support for Cursor 1.6+#1046

Merged
danielmeppiel merged 21 commits intomicrosoft:mainfrom
stbenjam:feat/cursor-commands
May 3, 2026
Merged

feat(cursor): add slash command support for Cursor 1.6+#1046
danielmeppiel merged 21 commits intomicrosoft:mainfrom
stbenjam:feat/cursor-commands

Conversation

@stbenjam
Copy link
Copy Markdown
Contributor

@stbenjam stbenjam commented Apr 29, 2026

Summary

  • Register the commands primitive in the Cursor target profile so apm install deploys .prompt.md files to .cursor/commands/*.md when a .cursor/ directory is present
  • Update documentation across 4 files to reflect Cursor command support
  • Add unit and integration tests covering the full dispatch path

Design rationale

Cursor 1.6 introduced custom slash commands stored as plain Markdown in .cursor/commands/. Cursor has since de-emphasized commands in favor of skills/rules, but as long as it supports the commands surface it makes sense to deploy to it.

Frontmatter strategy: Cursor itself does not consume YAML frontmatter -- the file is just a context-to-LLM routing mechanism. We deliberately emit Claude-compatible frontmatter (description, allowed-tools, arguments, argument-hint) rather than stripping it. In testing, leaving structured input hints in the file makes commands noticeably more successful at inferring required inputs from the user. The frontmatter is harmless to Cursor.

Implementation: This reuses the existing integrate_command() path (the else branch in CommandIntegrator.integrate_commands_for_target), so the only production code change is 3 lines in targets.py adding a PrimitiveMapping.

Test plan

  • Unit tests: opt-in guard, single/multi deploy, frontmatter preservation, sync removal, missing-dir graceful no-op (TestCursorCommandIntegration, 6 tests)
  • Integration test: target gating dispatch with mocked integrators confirms commands are dispatched for Cursor target (test_cursor_target_dispatches_commands)
  • End-to-end integration test: full integrate_package_primitives dispatch with real integrators, verifying .cursor/commands/review.md output including frontmatter content (TestCursorCommandEndToEnd)
  • Exhaustiveness check: test_partition_parity_with_old_buckets updated with commands_cursor bucket
  • Full suite: 5784 passed

Register the "commands" primitive in the Cursor target profile so
apm install deploys .prompt.md files to .cursor/commands/*.md
when a .cursor/ directory is present.

Cursor 1.6 introduced custom slash commands [1] stored as plain
Markdown in .cursor/commands/. Cursor has since de-emphasized
commands in favor of skills/rules [2], but as long as it supports
the commands surface it makes sense to deploy to it.

Cursor itself does not consume YAML frontmatter, but we
deliberately emit Claude-compatible frontmatter (description,
allowed-tools, arguments, argument-hint) rather than stripping it.
The frontmatter is harmless to Cursor -- the file is just a
context-to-LLM routing mechanism -- and in practice, leaving
structured input hints in the file makes the commands noticeably
more successful at inferring required inputs from the user.

This reuses the existing integrate_command() path (the else branch
in CommandIntegrator.integrate_commands_for_target), so the only
production code change is three lines in targets.py.

Testing strategy:
- Unit tests: opt-in guard, single/multi deploy, frontmatter
  preservation, sync removal, missing-dir graceful no-op
- Integration tests: target gating dispatch (mocked integrators)
  and full end-to-end through integrate_package_primitives with
  real integrators verifying file output

[1] https://cursor.com/changelog/1-6
[2] https://cursor.com/docs/plugins

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@stbenjam stbenjam requested a review from danielmeppiel as a code owner April 29, 2026 15:20
Copilot AI review requested due to automatic review settings April 29, 2026 15:20
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 Cursor 1.6+ slash command support by wiring the existing commands primitive into the Cursor target profile, plus updates docs and tests to reflect/cover the new dispatch path.

Changes:

  • Register commands in the Cursor target profile so .prompt.md can deploy to .cursor/commands/*.md when .cursor/ exists.
  • Update docs and guide content to mention Cursor command deployment.
  • Extend unit/integration tests and bucket-parity expectations to include commands_cursor.

Reviewed changes

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

Show a summary per file
File Description
src/apm_cli/integration/targets.py Adds Cursor commands primitive mapping to enable command integration for Cursor targets.
tests/unit/integration/test_data_driven_dispatch.py Updates partition bucket parity expectations to include commands_cursor.
tests/unit/integration/test_command_integrator.py Adds dispatch + end-to-end tests for Cursor command integration behavior.
docs/src/content/docs/introduction/what-is-apm.md Documents Cursor support for .cursor/commands/.
docs/src/content/docs/introduction/how-it-works.md Lists Cursor native integration paths including .cursor/commands/.
docs/src/content/docs/integrations/ide-tool-integration.md Adds .cursor/commands/*.md to the Cursor integration table.
packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Updates package authoring guidance to mention Cursor command deployment.

Comment thread src/apm_cli/integration/targets.py Outdated
Comment thread packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Outdated
Comment thread tests/unit/integration/test_command_integrator.py Outdated
Comment thread tests/unit/integration/test_command_integrator.py Outdated
- Docs: change "frontmatter preserved" to "normalized to supported
  command frontmatter" -- the shared command transformer keeps only
  description, allowed-tools, model, and argument-hint, dropping
  unknown keys like author or custom fields
- Tests: rename test_frontmatter_preserved to
  test_frontmatter_normalized_to_supported_subset and assert that
  non-whitelisted fields (author, custom-field) are dropped
- Tests: fix test_sync_removes_apm_commands to use manifest-based
  sync (managed_files set) instead of the legacy *-apm.md glob
  fallback, matching how Cursor commands are actually deployed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@stbenjam
Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

@stbenjam
Copy link
Copy Markdown
Contributor Author

Ah, I haven't signed up for it. I'll let Claude fix.

# Conflicts:
#	packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
# Conflicts:
#	src/apm_cli/integration/targets.py
#	tests/unit/integration/test_data_driven_dispatch.py
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 7 out of 7 changed files in this pull request and generated 1 comment.


def test_cursor_target_dispatches_commands(self):
"""When targets=[cursor], commands must be dispatched."""
import tempfile, shutil
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

PEP 8: avoid importing multiple modules in a single import statement. This file already uses separate import lines (and has shutil/tempfile imported at module scope), so prefer import shutil and import tempfile (or just rely on the existing top-level imports) for consistency and linting.

Suggested change
import tempfile, shutil

Copilot uses AI. Check for mistakes.
Copilot AI and others added 4 commits April 30, 2026 17:35
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t#1046)

- targets.py: switch Cursor commands format_id from cursor_command to
  claude_command and document why. The shared command transformer is
  what actually runs today; the previous tag mis-advertised a Cursor-
  specific writer that does not exist and would silently lie about
  preserving Cursor-only frontmatter (author/input/mcp/parameters).
  Switch back to a dedicated cursor_command tag only when an integrator
  branch is added that preserves full Cursor metadata verbatim.
- test_command_integrator.py: drop redundant per-test reimports of
  shutil/tempfile (PEP 8 nit, both are imported at module top).

Other Copilot comments were already covered:
- docs already say 'normalized to supported command frontmatter'
- test_sync_removes_managed_commands exercises Cursor manifest mode
- test_frontmatter_normalized_to_supported_subset asserts non-whitelisted
  fields (author, custom-field) are dropped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Three panelists converge on a silent frontmatter-drop bug, supply-chain raises a path traversal and an IDE code-execution consent gap, and a broken test assertion rounds out the blockers. All are fixable in a single revision pass.

Required before merge (10 items)

  • [python-architect] TestCursorCommandIntegration stub tests committed with only ellipsis bodies at tests/unit/integration/test_command_integrator.py:595

    • Why: Two classes (TestCursorCommandEndToEnd and TestCursorCommandIntegration) duplicate _make_package() verbatim, violating DRY. Extract to a shared pytest fixture or module-level helper.
    • Suggested fix: Extract _make_package into a module-level function or a shared conftest fixture; have both test classes call it.
  • [python-architect] format_id 'claude_command' used for Cursor target but no branch handles cursor-specific semantics at src/apm_cli/integration/command_integrator.py:224

    • Why: integrate_commands_for_target dispatches only on 'gemini_command'; all others fall through to integrate_command(), which emits Claude-branded diagnostic messages during Cursor installs. No tracking issue or TODO reference makes this debt invisible.
    • Suggested fix: Add a # TODO(cursor-command-format): <ticket-URL> comment alongside the existing targets.py note, and make integrate_command() accept the target name so diagnostic messages are target-agnostic.
  • [python-architect] test_partition_parity_with_old_buckets expects 'commands_cursor' bucket but dispatch.py produces no such key at tests/unit/integration/test_data_driven_dispatch.py:300

    • Why: The test asserts 'commands_cursor' is a key in BaseIntegrator.partition_managed_files(), but if dispatch.py does not emit that key the test will fail at runtime -- a broken-test merge.
    • Suggested fix: Either add 'commands_cursor' bucket emission to BaseIntegrator.partition_managed_files / dispatch.py, or remove the assertion until that work lands. Run pytest tests/unit/integration/test_data_driven_dispatch.py::TestPartitionManagedFiles::test_partition_parity_with_old_buckets to confirm.
  • [cli-logging-expert] Cursor-specific frontmatter silently dropped with no user-visible diagnostic at src/apm_cli/integration/command_integrator.py:208

    • Why: When deploying .prompt.md to .cursor/commands/ via the claude_command transformer, keys like author, mcp, parameters are stripped without any warning. This violates Signal-to-noise: a lossy transform affecting the deployed artifact must surface a diagnostics.warn() so users can act on it.
    • Suggested fix: After _transform_prompt_to_command, detect when the source file contains frontmatter keys not preserved by the transformer and call diagnostics.warn() once per file, e.g. 'Cursor command {name}: Cursor-specific frontmatter keys (author, mcp, parameters) were not written; claude_command transformer preserves only description, allowed-tools, model, argument-hint.'
  • [devx-ux-expert] Cursor-specific frontmatter keys are silently dropped with no install-time warning at src/apm_cli/integration/command_integrator.py:180

    • Why: The claude_command transformer silently discards Cursor-native keys (author, input, mcp, parameters). This violates the core mental model "install adds, never silently mutates" and "Failure mode is the product". A package author gets no signal that those fields were stripped.
    • Suggested fix: Collect unknown keys (set(post.metadata) - KNOWN_CLAUDE_KEYS) after building claude_metadata and emit a named warning listing dropped keys per file.
  • [devx-ux-expert] No install-time feedback when Cursor command deployment is skipped (no .cursor/ dir) at src/apm_cli/integration/command_integrator.py:345

    • Why: Silent skipping fails discoverability. A user who runs apm install gets no signal that Cursor commands were not deployed because .cursor/ is absent. CI pipelines with no .cursor/ dir silently diverge from a dev machine that has one.
    • Suggested fix: When cursor commands_dir parent (.cursor/) does not exist, emit a one-line note: 'Skipped .cursor/commands/ -- create a .cursor/ directory to enable Cursor command deployment.'
  • [devx-ux-expert] Reusing claude_command transformer for Cursor is a named-abstraction leak at src/apm_cli/integration/targets.py:336

    • Why: The input: key -- valid Cursor frontmatter -- is mapped to Claude argument semantics (generating argument-hint placeholders) instead of being passed through verbatim. A package author who writes input: expecting Cursor's native input handling gets silently rewritten behavior. This violates the familiarity principle.
    • Suggested fix: Implement a cursor_command transformer (even a thin pass-through preserving all keys verbatim), or explicitly document and add a test asserting that Cursor's input: frontmatter is intentionally re-mapped so the behavior is not accidental.
  • [supply-chain-security-expert] base_name derived from package filename is not validated before path join at src/apm_cli/integration/command_integrator.py:358

    • Why: base_name is extracted from prompt_file.name (a package-supplied filename) and joined directly into commands_dir without calling validate_path_segments(). A malicious package could ship ../../evil.prompt.md; base_name becomes ../../evil and target_path escapes the commands directory. This violates the Containment review lens.
    • Suggested fix: Call validate_path_segments(base_name, context='command filename') before constructing target_path, and follow with ensure_path_within(target_path, commands_dir) after the join.
  • [supply-chain-security-expert] find_files_by_glob rejects symlinks but does not ensure resolved path stays within package_path at src/apm_cli/integration/base_integrator.py:520

    • Why: find_files_by_glob() skips symlinks but does not call ensure_path_within(resolved, package_path). A package could include a hardlink pointing outside apm_modules/; hardlinks are not symlinks so the is_symlink() check passes. This is a path traversal vector (threat Added ghe support #7) shared by all integrators including the new cursor commands path.
    • Suggested fix: After resolved = f.resolve(), add: if not resolved.is_relative_to(package_path.resolve()): continue -- or use ensure_path_within(resolved, package_path) and catch PathTraversalError.
  • [supply-chain-security-expert] Cursor executes deployed .cursor/commands/*.md as slash commands -- post-install code execution via IDE at src/apm_cli/integration/targets.py:336

    • Why: Threat Add GitHub Enterprise (GHE) hostname support to dependency parsing #8: post-install code execution without explicit user opt-in. Deploying package-supplied content to .cursor/commands/ causes Cursor to make those commands available as slash commands. A compromised package maintainer can ship a malicious .prompt.md that injects instructions or exfiltrates context when a developer invokes it. Users receive no consent signal distinguishing "file copied" from "executable command registered in your IDE".
    • Suggested fix: Surface a distinct install-time warning that commands deployed to .cursor/commands/ will be executable as Cursor slash commands. Consider requiring an explicit opt-in flag (e.g. --allow-commands) or a manifest field in apm.yml to enable command deployment.

Nits (14 items, skip if you want)

  • [python-architect] Two import-cleanup hunks remove 'import shutil' and 'import tempfile' -- confirm module-level imports cover these at tests/unit/integration/test_command_integrator.py:491
  • [python-architect] TestCursorCommandEndToEnd._make_package is identical to TestCursorCommandIntegration._make_package -- move to module-level helper at tests/unit/integration/test_command_integrator.py:576
  • [python-architect] Cursor target TargetProfile comment has no tracking issue reference at src/apm_cli/integration/targets.py:329
  • [cli-logging-expert] diagnostics.info message hardcodes 'Claude arguments' even for Cursor deployments at src/apm_cli/integration/command_integrator.py:225
  • [cli-logging-expert] integrate_command docstring still references .claude/commands/ after Cursor reuse at src/apm_cli/integration/command_integrator.py:195
  • [devx-ux-expert] package-authoring.md lists Cursor commands support without noting the frontmatter subset limitation
  • [devx-ux-expert] Opt-in mechanism (.cursor/ dir presence) not mentioned in any user-facing doc at docs/src/content/docs/integrations/ide-tool-integration.md
  • [supply-chain-security-expert] commands_cursor missing from _BUCKET_ALIASES in base_integrator.py at src/apm_cli/integration/base_integrator.py:182
  • [supply-chain-security-expert] Silent skip of Cursor-specific frontmatter fields reduces auditability at src/apm_cli/integration/targets.py:329
  • [oss-growth-hacker] No CHANGELOG entry for Cursor command deployment at CHANGELOG.md:10
  • [oss-growth-hacker] package-authoring.md lists targets but has no runnable example for Cursor showing deployed command invocation
  • [oss-growth-hacker] targets.py inline comment leaks implementation uncertainty -- collapse to single-line TODO referencing a tracking issue at src/apm_cli/integration/targets.py:329
  • [oss-growth-hacker] hello-world template README does not mention Cursor slash command deployment
  • [oss-growth-hacker] what-is-apm.md comparison table misses the "write once, deploy everywhere" story angle at docs/src/content/docs/introduction/what-is-apm.md:151

CEO arbitration

Three independent panelists (cli-logging-expert, devx-ux-expert, supply-chain-security-expert) converge on the same silent-drop failure: Cursor-specific frontmatter keys are discarded without any user-visible diagnostic. This is the PR's most consequential defect -- it violates APM's core "install adds, never silently mutates" contract and will erode package-author trust the first time someone ships author, mcp, or parameters keys and sees them vanish without explanation. The fix is mechanical: detect stripped keys post-transform and emit a diagnostics.warn() per file. Supply-chain-security adds a harder blocker: base_name is path-joined without validate_path_segments(), creating a path traversal vector via malicious package filenames. This is a containment failure that must be closed before any Cursor-target code ships. The third security required -- that deploying to .cursor/commands/ registers IDE-executable slash commands without explicit user consent -- is the most strategically sensitive: it is not a bug in the traditional sense but a trust boundary that, if crossed silently, hands critics a "APM injects commands into your IDE" narrative that community trust cannot easily recover from. An install-time consent signal or --allow-commands opt-in is the minimum bar.

Python-architect raises two additional required items: the test_partition_parity_with_old_buckets assertion on commands_cursor will fail at runtime if dispatch.py does not emit that key, which is a broken-test merge as written; and the duplicated _make_package() helper across two test classes means the test suite carries unnecessary maintenance debt. The reuse of claude_command format_id for a Cursor target is flagged by three panelists at different severity levels; the consensus position is that a cursor_command format (even a thin pass-through) is the right long-term abstraction, but a clearly documented TODO with a tracking issue is an acceptable interim gate, provided the Claude-branded diagnostic messages are neutralized before merge.

Strategically, this PR carries the highest growth leverage of any recent commit -- Cursor plus Claude Code represents the majority of AI-native IDE users, and "one .prompt.md deploys everywhere" is a genuinely differentiated story. The required issues are all fixable in a single revision pass. Blocking on them is the right call: shipping a silent data-loss bug or a path traversal in the first Cursor integration would damage APM's credibility with the exact audience this feature is designed to win.

Dissent resolved: devx-ux-expert elevated the cursor_command abstraction leak to required and called for blocking merge until a native transformer exists; python-architect and cli-logging-expert treated the same issue as required-with-TODO (trackable debt acceptable for merge). Arbitration sides with the middle position: a TODO with a tracking issue URL neutralizes the abstraction leak as a merge blocker, but Claude-branded log messages must be made target-neutral before merge, which closes the UX panelist's core concern without requiring a full new transformer in this PR.

Growth/positioning note: Cursor commands parity is the strongest "write once, use everywhere" story APM has shipped since multi-target deploy. Claude Code + Cursor together account for the majority of AI-native IDE users. Recommend a dedicated CHANGELOG entry and a social post framing "one .prompt.md = slash commands in Claude Code AND Cursor, zero config" timed to the next version cut. A Cursor partnership co-announcement, if a contact exists, would amplify the story significantly.


Per-persona findings (full)

Python Architect

classDiagram
    class BaseIntegrator {
        +partition_managed_files(managed_files) dict
        +check_collision(...) bool
        +sync_remove_files(...) dict
    }
    class CommandIntegrator {
        +integrate_commands_for_target(target, pkg_info, root) IntegrationResult
        +integrate_command(source, target, ...) int
        +sync_for_target(target, pkg, root) dict
        -_transform_prompt_to_command(source) tuple
        -_write_gemini_command(source, target) void
    }
    class TargetProfile {
        +name: str
        +root_dir: str
        +primitives: dict
        +auto_create: bool
        +detect_by_dir: bool
    }
    class PrimitiveMapping {
        +subdir: str
        +extension: str
        +format_id: str
    }
    class IntegrationResult {
        +files_integrated: int
        +files_skipped: int
        +target_paths: list
    }
    BaseIntegrator <|-- CommandIntegrator
    CommandIntegrator ..> TargetProfile : reads primitives
    TargetProfile *-- PrimitiveMapping
    CommandIntegrator ..> IntegrationResult : returns
    note for PrimitiveMapping "cursor target uses format_id='claude_command' (shared transformer, known debt)"
    class CommandIntegrator:::touched
    class PrimitiveMapping:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A[apm install command] --> B[integrate_package_primitives]
    B --> C{target has 'commands' primitive?}
    C -- No --> Z[return empty IntegrationResult]
    C -- Yes --> D{auto_create=False AND .cursor/ missing?}
    D -- skip --> Z
    D -- proceed --> E["[FS] find_files_by_glob in .apm/prompts/"]
    E --> F{for each .prompt.md}
    F --> G[check_collision]
    G -- collision --> H[skip file]
    G -- clear --> I{format_id == 'gemini_command'?}
    I -- Yes --> J["[FS] _write_gemini_command TOML"]
    I -- No --> K[integrate_command via claude_command transformer]
    K --> L[_transform_prompt_to_command]
    L --> M[resolve_links]
    M --> N[SecurityGate.scan_text]
    N --> O["[FS] write .cursor/commands/name.md"]
    O --> F
    F -- done --> P[return IntegrationResult]
Loading

Design patterns

  • Used in this PR: Data-Driven Configuration -- routing (directory, extension, transformer) is fully encoded in KNOWN_TARGETS["cursor"].primitives; no new integrator class required.
  • Used in this PR: Template Method -- integrate_commands_for_target calls check_collision, sync_remove_files, and find_prompt_files from BaseIntegrator; subclass only overrides the per-file write step.
  • Pragmatic suggestion: Introduce a cursor_command format_id as a thin pass-through transformer. The Strategy dispatch on format_id already implies this extension point; adding Cursor-specific logic to integrate_command() instead would violate open/closed.

Required:

  1. TestCursorCommandIntegration stub tests committed with only ellipsis bodies at tests/unit/integration/test_command_integrator.py:595 -- Two classes duplicate _make_package() verbatim, violating DRY. Extract to a shared pytest fixture or module-level helper.

  2. format_id 'claude_command' used for Cursor target but no branch handles cursor-specific semantics at src/apm_cli/integration/command_integrator.py:224 -- Claude-branded log messages appear during Cursor installs; no tracking issue or TODO reference. Add # TODO(cursor-command-format): <ticket-URL> and make integrate_command() target-name-aware.

  3. test_partition_parity_with_old_buckets expects 'commands_cursor' bucket but dispatch.py produces no such key at tests/unit/integration/test_data_driven_dispatch.py:300 -- will fail at runtime; verify and add missing emission or remove assertion.

Nits:

  • Two import-cleanup hunks remove 'import shutil' and 'import tempfile' -- confirm module-level imports cover these at tests/unit/integration/test_command_integrator.py:491
  • TestCursorCommandEndToEnd._make_package is identical to TestCursorCommandIntegration._make_package -- move to module-level helper at tests/unit/integration/test_command_integrator.py:576
  • Cursor target TargetProfile comment has no tracking issue reference -- add # TODO: <issue-URL> at src/apm_cli/integration/targets.py:329

CLI Logging Expert

Required:

  1. Cursor-specific frontmatter silently dropped with no user-visible diagnostic at src/apm_cli/integration/command_integrator.py:208 -- After _transform_prompt_to_command, detect stripped keys and call diagnostics.warn() once per file: 'Cursor command {name}: Cursor-specific frontmatter keys (author, mcp, parameters) were not written; claude_command transformer preserves only description, allowed-tools, model, argument-hint.'

Nits:

  • diagnostics.info message hardcodes 'Claude arguments' even for Cursor deployments at src/apm_cli/integration/command_integrator.py:225 -- Change to target-neutral: f"Mapped input -> arguments in {target.name}: [...]"
  • integrate_command docstring still references .claude/commands/ after Cursor reuse at src/apm_cli/integration/command_integrator.py:195 -- Update to reference both Claude and Cursor targets.

DevX UX Expert

Required:

  1. Cursor-specific frontmatter keys are silently dropped with no install-time warning at src/apm_cli/integration/command_integrator.py:180 -- Collect unknown keys (set(post.metadata) - KNOWN_CLAUDE_KEYS) and emit a named warning listing dropped keys per file.

  2. No install-time feedback when Cursor command deployment is skipped (no .cursor/ dir) at src/apm_cli/integration/command_integrator.py:345 -- Emit a one-line note: 'Skipped .cursor/commands/ -- create a .cursor/ directory to enable Cursor command deployment.'

  3. Reusing claude_command transformer for Cursor is a named-abstraction leak at src/apm_cli/integration/targets.py:336 -- input: frontmatter is silently rewritten to Claude argument semantics. Implement a cursor_command transformer (even a thin pass-through), or document and test the rewrite behavior explicitly.

Nits:

  • package-authoring.md lists Cursor commands support without noting the frontmatter subset limitation -- add '(frontmatter normalized to: description, allowed-tools, model, argument-hint -- other keys are dropped)'
  • Opt-in mechanism (.cursor/ dir presence) not mentioned in any user-facing doc at docs/src/content/docs/integrations/ide-tool-integration.md -- add callout: 'Commands are deployed only when a .cursor/ directory exists in the project root.'

Supply Chain Security Expert

Required:

  1. base_name derived from package filename is not validated before path join at src/apm_cli/integration/command_integrator.py:358 -- Call validate_path_segments(base_name, context='command filename') before constructing target_path, and ensure_path_within(target_path, commands_dir) after the join.

  2. find_files_by_glob rejects symlinks but does not ensure resolved path stays within package_path at src/apm_cli/integration/base_integrator.py:520 -- After resolved = f.resolve(), add: if not resolved.is_relative_to(package_path.resolve()): continue

  3. Cursor executes deployed .cursor/commands/*.md as slash commands -- post-install code execution via IDE at src/apm_cli/integration/targets.py:336 -- Surface a distinct install-time warning that deployed commands will be Cursor-executable. Consider --allow-commands opt-in or a manifest field in apm.yml.

Nits:

  • commands_cursor missing from _BUCKET_ALIASES in base_integrator.py at src/apm_cli/integration/base_integrator.py:182 -- Add 'commands_cursor' to _BUCKET_ALIASES; orphan files in .cursor/commands/ may not be cleaned up otherwise.
  • Silent skip of Cursor-specific frontmatter fields reduces auditability at src/apm_cli/integration/targets.py:329 -- Log a per-file diagnostic listing dropped frontmatter keys.

Auth Expert

Inactive -- PR only modifies targets.py (PrimitiveMapping addition for Cursor commands), test files, and documentation; no auth, token, credential, or host-classification code is touched.

OSS Growth Hacker

No findings.

Nits:

  • No CHANGELOG entry for Cursor command deployment -- add under [Unreleased] > Added: '**Cursor slash commands.** APM packages now deploy .prompt.md files to .cursor/commands/ automatically when a .cursor/ directory is detected.'
  • package-authoring.md lists targets but has no runnable example for Cursor -- add one-line callout: 'In Cursor, deployed commands appear under Cmd+K > Slash Commands as the description value from your frontmatter.'
  • targets.py inline comment leaks implementation uncertainty -- collapse to single-line TODO referencing a tracking issue at src/apm_cli/integration/targets.py:329
  • hello-world template README does not mention Cursor slash command deployment -- add: 'If your project has a .cursor/ directory, this prompt also appears as a Cursor slash command automatically.'
  • what-is-apm.md comparison table misses the "write once, deploy everywhere" story angle at docs/src/content/docs/introduction/what-is-apm.md:151 -- add callout: 'A single .prompt.md deploys as a slash command to Claude Code (.claude/commands/) and Cursor (.cursor/commands/) simultaneously.'

Verdict computed deterministically: 10 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.

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 #1046 · ● 2.9M ·

@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
…target-aware diagnostics

Address all 10 required findings from APM Review Panel on PR microsoft#1046:

- Extract _make_package() to module-level test helper (DRY across 6 classes)
- Make integrate_command() target-aware via target_name kwarg; emit
  'command arguments' instead of Claude-branded message for Cursor installs
- Detect frontmatter keys not preserved by the shared claude_command
  transformer (author, mcp, parameters, ...) and surface diagnostics.warn()
  per file so users see the lossy transform at install time
- Add install-time info note when .cursor/ is missing instead of silently
  skipping deployment (improves CI-vs-dev discoverability)
- Validate base_name with validate_path_segments() and ensure_path_within()
  before joining into commands_dir to close path-traversal vector via
  malicious package filenames
- Harden find_files_by_glob() to reject hardlinks/files whose resolved path
  escapes package_path (symlink check did not catch hardlinks)
- Surface a one-time IDE consent warning per package when Cursor commands
  are deployed since they become invokable as IDE slash commands
- Add TODO(cursor-command-format) with tracking issue URL on both targets.py
  and integrate_command() to make the shared-transformer debt trackable
- Add commands_cursor identity entry to _BUCKET_ALIASES for explicit
  documentation parity with commands_opencode
- CHANGELOG entry under [Unreleased] and ide-tool-integration.md note
  about the frontmatter subset limitation
- New TestCursorCommandPanelFindings regression class covers all five
  panel-driven behaviours (dropped-key warn, traversal rejection,
  target-aware diagnostics, IDE consent, skip note)

Verified: 6939 unit tests pass; ruff format + ruff check clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Architecture is sound but five required findings -- spanning consent model, security symmetry, and docs -- block merge.

Required before merge (5 items)

  • [python-architect] Hardcoded if target.name == "cursor" consent-warning violates the Strategy pattern at src/apm_cli/integration/command_integrator.py:439

    • Why: The TargetProfile / PrimitiveMapping layer is the correct place to carry a requires_executable_consent: bool flag. As more targets are added or renamed, the name-string branch will silently miss. This is exactly the coupling the BaseIntegrator / TargetProfile strategy pattern was designed to eliminate.
    • Suggested fix: Add requires_executable_consent: bool = False to TargetProfile (or PrimitiveMapping). Set it to True on the cursor entry in targets.py. Replace if target.name == "cursor" with if target.requires_executable_consent. Zero name-string coupling; new targets opt in via the flag.
  • [devx-ux-expert] cli-commands.md not updated at docs/src/content/docs/reference/cli-commands.md

    • Why: apm install now deploys to .cursor/commands/ when .cursor/ exists -- a behavior change to the install command's deployment targets. A user reading cli-commands.md will not know Cursor slash commands are being registered. Per persona contract: if a CLI change is not reflected in cli-commands.md in the same PR, that change is incomplete by definition.
    • Suggested fix: Add a note under apm install documenting the Cursor slash command opt-in behavior: when .cursor/ exists, .prompt.md files are deployed to .cursor/commands/*.md. Mirror the existing pattern used for Claude / OpenCode in that doc.
  • [devx-ux-expert] Consent warning is hardcoded to cursor only -- inconsistent with Claude Code which performs identical deployment silently at src/apm_cli/integration/command_integrator.py:431

    • Why: Claude Code writes .prompt.md files to .claude/commands/ so they become IDE-invokable slash commands without any consent warning. A user who installed Claude commands without seeing a warning will be surprised to see one for Cursor. The asymmetry violates the Familiarity lens: behaviour the user has already seen for Claude should behave the same way for Cursor. If the consent warning is right for security, it should be applied uniformly.
    • Suggested fix: Either (a) remove the Cursor-specific consent warning and rely on the lossy-transform warning + skip-note, matching Claude Code behavior; or (b) extract the consent warning into a shared helper and apply it uniformly for every target that writes slash commands (claude, cursor, opencode, gemini).
  • [supply-chain-security-expert] warn() is not a consent gate for post-install code execution (Threat Add GitHub Enterprise (GHE) hostname support to dependency parsing #8) at src/apm_cli/integration/command_integrator.py

    • Why: Deploying .md files that become immediately invokable as Cursor slash commands is functionally equivalent to post-install code execution from the IDE's perspective. A warn() diagnostic that surfaces and is then ignored is notification, not consent. A user running apm install in CI, a script, or a non-verbose shell may never act on the warning. The threat model requires explicit user opt-in, not passive disclosure.
    • Suggested fix: Require an explicit opt-in flag (e.g., --allow-cursor-commands or a per-package allowlist entry in apm.yml) before deploying to the Cursor commands directory. Warn AND gate: if the flag is absent, skip deployment and emit a warning that explains how to enable it. This mirrors how package managers handle lifecycle scripts (npm --ignore-scripts).
  • [supply-chain-security-expert] Asymmetric consent model: Claude/OpenCode/Gemini commands receive no warning despite identical invocability risk at src/apm_cli/integration/command_integrator.py

    • Why: If Cursor slash commands warrant a consent signal because they "become invokable inside the IDE", the same logic applies to Claude prompt files, OpenCode commands, and Gemini commands. The asymmetry means a malicious package targeting Claude users gets no warning at all, creating a bypass: an attacker simply avoids the "cursor" target name. Security controls must be applied uniformly or adversaries will route around the one exception.
    • Suggested fix: Apply the same consent gate to all targets that deploy files that become invokable by an AI or IDE. If the PR scope is intentionally limited to Cursor, open a tracked issue and add a TODO referencing it so the gap is not silently accepted.

Nits (11 items, skip if you want)

  • [python-architect] pkg_resolved = package_path.resolve() recomputed inside inner file-iteration loop at src/apm_cli/integration/base_integrator.py:521
  • [python-architect] _transform_prompt_to_command returns an anonymous 4-tuple -- use a NamedTuple or dataclass at src/apm_cli/integration/command_integrator.py:133
  • [python-architect] "commands_cursor": "commands_cursor" is a no-op self-alias in _BUCKET_ALIASES at src/apm_cli/integration/base_integrator.py:183
  • [cli-logging-expert] Hardcoded preservation list in dropped-key warning can drift from _PRESERVED_COMMAND_KEYS at src/apm_cli/integration/command_integrator.py:226
  • [cli-logging-expert] command(s) pluralization violates 'use exact counts' rule at src/apm_cli/integration/command_integrator.py:288
  • [cli-logging-expert] Path traversal rejection warnings omit actionable fix for package authors at src/apm_cli/integration/command_integrator.py:318
  • [cli-logging-expert] target_name.capitalize() can silently mangle multi-word or camelCase target names at src/apm_cli/integration/command_integrator.py:226
  • [devx-ux-expert] Skip-info diagnostic fires per-package even when the package ships no prompt files at src/apm_cli/integration/command_integrator.py:400
  • [devx-ux-expert] Dropped-key warning says input is "preserved" but input is consumed and transformed, not passed through at src/apm_cli/integration/command_integrator.py:264
  • [supply-chain-security-expert] Hardlink containment guard silently continues on OSError without logging at src/apm_cli/integration/base_integrator.py
  • [oss-growth-hacker] CHANGELOG entry leads with mechanism, not user benefit; table cell for .cursor/commands is a run-on paragraph; no end-to-end runnable example in docs; Cursor de-emphasis of commands is a strategic time-bomb not documented externally

CEO arbitration

The panel surfaces a coherent cluster of required findings that collectively demand a REJECT verdict. The two most structurally serious findings -- raised independently by supply-chain-security-expert and echoed architecturally by python-architect and devx-ux-expert -- concern the consent model for cursor command deployment. A warn() call is not a consent gate; it is a notification. When a post-install step deploys executable command files into an IDE's command directory, silent continuation after a warning is functionally indistinguishable from no warning at all. This must become an explicit opt-in flag before the change ships. Compounding this, the asymmetric treatment of Cursor versus Claude Code, OpenCode, and Gemini -- all of which deploy identically invocable commands without any warning -- both undermines the security rationale and creates an attacker bypass route: simply declare a non-Cursor target. The hardcoded name-string branch that python-architect flags is the mechanical root of this asymmetry; the TargetProfile layer is the correct home for a requires_executable_consent flag, and fixing the architecture fixes the security gap simultaneously. devx-ux-expert's documentation finding stands independently: cli-commands.md omits a behavior change that affects every user who reads it, and that omission must be resolved regardless of the consent gate decision. Taken together, these five required findings across four active panelists form a coherent, mutually reinforcing case for rejection; none can be deferred as polish.

Dissent resolved: cli-logging-expert and oss-growth-hacker filed zero required findings. On the consent question, this CEO sides with supply-chain-security-expert and devx-ux-expert: the distinction between notification and consent is not a matter of taste but of threat modeling, and the asymmetric model compounds the risk rather than mitigating it.

Growth/positioning note: oss-growth-hacker's framing is the right external story: APM as the package manager that bridges AI packages to native IDE slash commands is differentiated and Cursor mindshare is currently high. The compounding effect for package authors is real. Two risks to action before launch: the CHANGELOG and docs are written for maintainers, not for the developer discovering APM for the first time -- rewrite the CHANGELOG entry around user benefit, not mechanism; and Cursor's own signals suggest commands are a legacy surface, so shipping skills/rules support before Cursor deprecates commands would materially strengthen the narrative.


Per-persona findings (full)

Python Architect

classDiagram
    class TargetProfile {
        +name: str
        +root_dir: str
        +auto_create: bool
        +primitives: dict[str, PrimitiveMapping]
    }
    class PrimitiveMapping {
        +subdir: str
        +extension: str
        +format_id: str
        +deploy_root: str | None
    }
    class BaseIntegrator {
        +find_files_by_glob()
        +check_collision()
        +resolve_links()
        +partition_bucket_key()
    }
    class CommandIntegrator {
        +find_prompt_files()
        +_transform_prompt_to_command() tuple
        +integrate_command(target_name)
        +integrate_commands_for_target()
        +sync_for_target()
    }
    class _PRESERVED_COMMAND_KEYS {
        <<frozenset>>
        description
        allowed-tools
        model
        argument-hint
        input
    }
    class DiagnosticCollector {
        +warn()
        +info()
        +security()
    }
    BaseIntegrator <|-- CommandIntegrator
    TargetProfile "1" *-- "many" PrimitiveMapping
    CommandIntegrator ..> TargetProfile : reads
    CommandIntegrator ..> DiagnosticCollector : emits
    CommandIntegrator ..> _PRESERVED_COMMAND_KEYS : filters with
    class CommandIntegrator:::touched
    class _PRESERVED_COMMAND_KEYS:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A[integrate_commands_for_target\ntarget=cursor] --> B{mapping = target.primitives.get commands}
    B -- None --> Z[return empty IntegrationResult]
    B -- exists --> C{auto_create=False\nand .cursor/ missing?}
    C -- yes --> D["[FS] diagnostics.info: skipped, create dir"]
    D --> Z
    C -- no --> E["[FS] find_prompt_files via find_files_by_glob"]
    E --> F{prompt_files empty?}
    F -- yes --> Z
    F -- no --> G[init_link_resolver]
    G --> H{target.name == cursor?}
    H -- yes --> I[diagnostics.warn: consent warning]
    I --> J
    H -- no --> J[for each prompt_file]
    J --> K[validate_path_segments base_name]
    K -- PathTraversalError --> L[diagnostics.warn + skip]
    K -- ok --> M["[FS] build target_path = commands_dir/base.md"]
    M --> N[ensure_path_within target_path, commands_dir]
    N -- PathTraversalError --> L
    N -- ok --> O{check_collision?}
    O -- collision --> L
    O -- ok --> P{format_id == gemini_command?}
    P -- yes --> Q["[FS] _write_gemini_command"]
    P -- no --> R[integrate_command\nsource, target, target_name=cursor]
    R --> R1[_transform_prompt_to_command]
    R1 --> R2[dropped_keys = source_keys - _PRESERVED_COMMAND_KEYS]
    R2 --> R3{dropped_keys?}
    R3 -- yes --> R4[diagnostics.warn: lossy transform]
    R3 -- no --> R5
    R4 --> R5["[FS] resolve_links in content"]
    R5 --> R6[SecurityGate.scan_text]
    R6 --> R7["[FS] write .cursor/commands/name.md"]
    Q --> S[files_integrated plus 1]
    R7 --> S
    S --> J
    J -- done --> T[return IntegrationResult]
Loading

Design patterns

  • Used in this PR: Strategy + Data-Driven Dispatch (TargetProfile/PrimitiveMapping) -- KNOWN_TARGETS registry drives which integrators fire and where files land at runtime, no if-chains per target. Template Method (BaseIntegrator) -- find_files_by_glob, check_collision, resolve_links are shared algorithms; CommandIntegrator contributes only the command-specific transform. Collect-then-Render (DiagnosticCollector) -- all warnings collected during the pass, rendered once after the install loop. Frozen Constant (_PRESERVED_COMMAND_KEYS frozenset) -- immutable, thread-safe, zero allocation per call.
  • Broken Pattern: Name-String Branch -- if target.name == "cursor" inside integrate_commands_for_target is a strategy pattern violation; the integrator should read a flag from TargetProfile.
  • Pragmatic suggestion: NamedTuple for _TransformResult -- the 4-tuple return from _transform_prompt_to_command has two list[str] elements; a NamedTuple costs nothing at runtime and self-documents the unpacking.

Required findings: 1 (see top)
Nits: 3 (see top)

CLI Logging Expert

No findings required.

Nits:

  • Hardcoded preservation list in warn message can drift from _PRESERVED_COMMAND_KEYS (derive from the frozenset instead)
  • command(s) should be properly pluralized using exact counts
  • Path traversal rejection warnings should include a fix hint for package authors
  • target_name.capitalize() can mangle multi-word or camelCase target names; use .title() or a display_name field

DevX UX Expert

Required findings: 2 (see top)

Nits:

  • Skip-info diagnostic fires per-package even when no prompt files exist; move the check after find_prompt_files() confirms prompts are present
  • Dropped-key warning says input is "preserved" but input is consumed by the argument mapper and becomes arguments; the message is misleading to package authors

Supply Chain Security Expert

Required findings: 2 (see top)

Nits:

  • Hardlink containment guard except (ValueError, OSError): continue swallows errors silently; log a warn diagnostic including the offending path so operators can detect anomalous install behavior
  • Consent warning detail string constructs the inspection path with forward-slash concatenation; use commands_dir.resolve() for an absolute, platform-correct path

Auth Expert

Inactive -- PR #1046 only modifies integration/command_integrator.py, integration/base_integrator.py, integration/targets.py, and test/doc files; no authentication, token management, or credential resolution code is touched.

OSS Growth Hacker

No findings required.

Nits:

  • CHANGELOG entry leads with mechanism ("shared command transformer preserves..."), not user benefit; reframe around the user outcome
  • No end-to-end runnable example in docs: install a package, open Cursor, type /, see the command
  • Table cell for .cursor/commands/*.md is a 50-word run-on paragraph; restore to one sentence and move opt-in/normalization detail to a callout block
  • Cursor has signalled commands are a legacy surface; add a stability callout in docs so users are not blindsided if Cursor changes behavior

Verdict computed deterministically: 5 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.

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 #1046 · ● 2.6M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
…efault, --allow-cursor-commands

Round 4 panel raised 8 required findings against the cursor-commands
gate. Round 5 addresses all of them:

Architecture (python-architect)
- Add frozen IntegrateOptions dataclass with extra_kwargs(prim) helper
  in install/services.py. Dispatch loop is now branchless — no more
  inline 'if _prim_name == "commands"' special case.
- Hoist pkg_name once at top of integrate_commands_for_target.
- Move executable-commands gate before find_prompt_files in the happy
  path; keep one find call inside the gate-skipped branch so the
  user-facing skip warning includes an exact count.

CLI logging (cli-logging-expert)
- _post_install_summary surfaces the consent flag in --verbose output:
  '  allow-cursor-commands: true|false' (gated on logger.verbose).
- Add _should_emit_passthrough_notice helper + _passthrough_notified
  instance set on CommandIntegrator. One-shot info diagnostic fires
  once per (install, target) on Cursor installs explaining that
  Cursor command files keep the Claude-compatible frontmatter subset
  intentionally for cross-tool compatibility.
- Combine missing-.cursor/-dir hint with consent-flag mention so users
  see both signals in one diagnostic.

DevX UX (devx-ux-expert)
- Rename CLI flag --allow-executable-commands -> --allow-cursor-commands
  (target-scoped, user-friendly). Internal pipeline parameter remains
  allow_executable_commands so future targets can share the gate
  plumbing via their own --allow-<target>-commands flags. Mapping
  happens at the CLI boundary in install().
- Update skip warning to name the new flag.

Supply-chain security (supply-chain-security-expert)
- Flip TargetProfile.requires_executable_consent default to True at
  the dataclass level. New targets now refuse command deployment unless
  an opt-out is explicit. Add explicit =False with rationale comment
  on each existing target (claude, opencode, gemini), each referencing
  enterprise/security.md.
- Add new '## Post-install code execution (Threat microsoft#8)' section to
  docs/src/content/docs/enterprise/security.md with the per-target
  posture matrix (cursor=gated, claude/opencode/gemini=opt-out),
  rationale for the asymmetry, npm --ignore-scripts mental model,
  and future-targets guidance.
- Remove dead bool() cast in should_deploy_executable_commands.
- Remove inline TODO comments referencing PR URL.

OSS growth (oss-growth-hacker)
- Rewrite CHANGELOG entry to lead with user outcome ('Cursor users
  can install APM package slash commands with one flag…'). Security
  rationale moved to end. Cursor 1.6+ lifecycle note included inline.
- Add Cursor 1.6+ lifecycle caveat to:
  - docs/.../integrations/ide-tool-integration.md
  - docs/.../reference/cli-commands.md
  - packages/apm-guide/.apm/skills/apm-usage/commands.md
  - packages/apm-guide/.apm/skills/apm-usage/package-authoring.md

Tests
- Update test assertions to use new flag name.
- Add requires_executable_consent=False to the synthetic test profile
  in tests/unit/integration/test_data_driven_dispatch.py (consequence
  of fail-closed default).
- Full unit suite: 6924 passed.

Round 5 panel: 6/6 specialists APPROVE; CEO synthesizer APPROVE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@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 Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

The technical implementation is clean and the security gate is well-designed; the REJECT is driven by two CHANGELOG communication failures that create community-trust risk on a feature the project itself flags as potentially short-lived.

Required before merge (2 items)

  • [oss-growth-hacker] CHANGELOG hook buries the value proposition under implementation noise at CHANGELOG.md:21

    • Why: The one-line hook is immediately swallowed by 150 words of frontmatter key behavior, transformer diagnostics, and threat model references. Existing users scanning the CHANGELOG will not extract a repostable claim. Release notes written for maintainers, not users -- a flagged anti-pattern per the growth-hacker persona.
    • Suggested fix: Lead with the user outcome in one sentence, then fold the implementation details after. Proposed hook: "Cursor slash commands: apm install --allow-cursor-commands now deploys package prompts as native Cursor slash commands. Opt-in required (mirrors npm --ignore-scripts). Cursor 1.6+ only." Everything else can follow.
  • [oss-growth-hacker] Lifecycle deprecation warning is buried at end of a 150-word CHANGELOG paragraph -- invisible at the critical decision point at CHANGELOG.md:21

    • Why: The note "Cursor is de-emphasizing commands in favor of rules/skills, so monitor Cursor release notes" appears as the last clause of the last sentence. A developer reading only the feature headline will opt in, deploy commands to their package, and ship. If Cursor removes the commands surface in 1.7, APM looks tone-deaf. The risk is real -- the CHANGELOG itself acknowledges it -- and needs to be a prominent callout, not a trailing subordinate clause.
    • Suggested fix: Promote the lifecycle warning to a clearly visible callout immediately after the one-line hook: "[!] Lifecycle note: Cursor 1.6+ only. Cursor is actively moving away from commands toward rules/skills; this surface may be deprecated in a future Cursor release. Monitor Cursor release notes before adopting this in published packages."

Nits (10 items, skip if you want)

  • [python-architect] IntegrateOptions constructed inside per-target loop with constant values at src/apm_cli/install/services.py -- hoist above the for _target in targets: loop to match docstring intent
  • [python-architect] extra_kwargs return type is unparameterized dict at src/apm_cli/install/services.py:71 -- change to -> dict[str, Any]
  • [python-architect] should_deploy_executable_commands lives in command_integrator but is documented as a shared gate -- track a follow-up to relocate to integration/consent.py before a second primitive needs a consent gate
  • [python-architect] CLI flag --allow-cursor-commands is target-specific but internal bool allow_executable_commands is generic -- add a TODO to move to frozenset[str] of allowed target names if a second consent-gated target arrives
  • [cli-logging-expert] Double-guard on verbose_detail is redundant at src/apm_cli/commands/install.py:1604 -- verbose_detail() already guards itself internally; remove the outer if logger.verbose:
  • [cli-logging-expert] command(s) hedging violates the exact-count rule at src/apm_cli/integration/command_integrator.py:520 -- use proper singular/plural: f"{n} {'command' if n == 1 else 'commands'}"
  • [devx-ux-expert] No env-var escape hatch documented for CI pipelines at docs/src/content/docs/reference/cli-commands.md:118 -- add APM_ALLOW_CURSOR_COMMANDS=1 mirroring APM_ALLOW_PROTOCOL_FALLBACK
  • [devx-ux-expert] Lifecycle deprecation note in flag bullet conflates current behavior with future uncertainty -- move to a > **Note:** admonition in ide-tool-integration.md
  • [devx-ux-expert] Warning message text is not quoted anywhere in the docs -- add a code block showing the exact warning string emitted when the flag is absent
  • [devx-ux-expert] --allow-cursor-commands interaction with --target is undocumented -- add a bullet: "When --target does not include cursor, this flag is accepted but has no effect"
  • [supply-chain-security-expert] allowed-tools passthrough to Cursor is unvalidated at src/apm_cli/integration/command_integrator.py:43 -- add a comment noting this key's semantics are Claude-specific and should be reviewed when a dedicated cursor_command formatter lands

CEO arbitration

The REJECT verdict is deterministic and correctly computed: the oss-growth-hacker raised two REQUIRED findings, both targeting CHANGELOG.md. The technical specialists -- python-architect, cli-logging-expert, devx-ux-expert, and supply-chain-security-expert -- returned a combined zero REQUIRED findings. That asymmetry is itself the strategic signal, not a reason to discount the growth-hacker's call.

The two REQUIRED findings are genuinely blocking. CHANGELOG.md is the highest-traffic artifact for existing adopters evaluating whether to act on a release. A feature that the project itself flags as potentially short-lived (Cursor is de-emphasizing commands) must earn its opt-in. If the hook does not hand the reader a repostable one-sentence value claim in the first line, adopters will either skip the feature or adopt it without understanding the risk surface. The lifecycle deprecation warning buried as the final clause of a 150-word paragraph is a community-trust failure waiting to happen. The recommended fix is low-cost and high-trust-leverage: a one-sentence hook and a prominently styled lifecycle callout. No code changes required to unblock APPROVE.

Growth/positioning note: The security framing is the actual story: APM treats prompt deployment like npm treats scripts -- opt-in, explicit, auditable. That one-line parallel is absent from every user-facing surface in this PR. The recommended CHANGELOG hook doubles as launch copy for a short thread. Classified as moderate signal given the Cursor longevity risk; hold the full launch beat for the rules/skills equivalent or for confirmed Cursor stability.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class TargetProfile {
        <<ValueObject>>
        +name str
        +root_dir str
        +primitives dict
        +auto_create bool
        +requires_flag str
        +requires_executable_consent bool
    }
    class PrimitiveMapping {
        <<ValueObject>>
        +subdir str
        +extension str
        +format_id str
    }
    class IntegrateOptions {
        <<ValueObject>>
        +allow_executable_commands bool
        +extra_kwargs(primitive_name) dict
    }
    class CommandIntegrator {
        +integrate_commands_for_target(target, pkg, root, allow_executable_commands)
        +integrate_package_commands(pkg, root, allow_executable_commands)
    }
    class InstallContext {
        <<Accumulator>>
        +project_root Path
        +force bool
        +verbose bool
        +allow_executable_commands bool
    }
    class InstallRequest {
        <<ValueObject>>
        +allow_executable_commands bool
    }
    TargetProfile *-- PrimitiveMapping : primitives
    CommandIntegrator ..> TargetProfile : reads requires_executable_consent
    InstallContext ..> CommandIntegrator : supplies via integrate phase
    IntegrateOptions ..> CommandIntegrator : extra_kwargs splat
    InstallRequest ..> InstallContext : seeds
    note for TargetProfile "fail-closed: default True\ncursor=True, claude/opencode/gemini=False"
    note for IntegrateOptions "Centralises per-primitive kwarg\nrouting; frozen so gate cannot\nbe mutated mid-loop (Threat #8)"
    class TargetProfile:::touched
    class IntegrateOptions:::touched
    class InstallContext:::touched
    class InstallRequest:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    CLI["apm install --allow-cursor-commands"] --> parse["commands/install.py\n_install_apm_dependencies()\nallow_cursor_commands=True"]
    parse --> optsbuild["InstallOptions.allow_executable_commands=True"]
    optsbuild --> pipeline["install/pipeline.py\nrun_install_pipeline(allow_executable_commands=True)"]
    pipeline --> ctx["InstallContext\n.allow_executable_commands=True"]
    ctx --> integrate["install/phases/integrate.py run()\npasses ctx.allow_executable_commands"]
    integrate --> svc["install/services.py\nintegrate_package_primitives(allow_executable_commands=True)"]
    svc --> opts["IntegrateOptions(allow_executable_commands=True)\n[constructed per-target in loop]"]
    opts --> loop["for target in targets\nfor prim_name in target.primitives"]
    loop --> extra["IntegrateOptions.extra_kwargs('commands')\n-> {allow_executable_commands: True}"]
    extra --> cmdint["CommandIntegrator\n.integrate_commands_for_target(..., allow_executable_commands=True)"]
    cmdint --> gate{"should_deploy_executable_commands(target,\nallow_executable_commands=True)"}
    gate -->|"requires_executable_consent=False\n(claude/opencode/gemini)"| deploy["[FS] write command files"]
    gate -->|"requires_executable_consent=True AND flag=True\n(cursor + --allow-cursor-commands)"| deploy
    gate -->|"requires_executable_consent=True AND flag=False\n(cursor without flag)"| skip["[I/O] find_prompt_files() once\ndiagnostics.warn(commands_skipped)\nreturn IntegrationResult(0,...)"]
Loading

Design patterns

  • Used in this PR: Dataclass-as-value-object -- IntegrateOptions (frozen) centralises per-primitive kwarg routing so the dispatch loop body stays unconditional; TargetProfile.requires_executable_consent is a fail-closed sentinel (default True) so new targets opt in to safety without explicit configuration.
  • Used in this PR: Strategy (dispatch via extra_kwargs) -- IntegrateOptions.extra_kwargs(primitive_name) acts as a lightweight strategy selector that shapes the kwargs injected into each integrator call, keeping the loop body uniform regardless of which primitive is being dispatched.
  • Pragmatic suggestion: If a second target-scoped consent flag arrives, replace allow_executable_commands: bool with executable_consent_targets: frozenset[str] on IntegrateOptions / InstallContext. At current scope this change is premature -- but the slot exists on IntegrateOptions to make it a one-field swap rather than a structural refactor.

Nits:

  • IntegrateOptions constructed inside per-target loop with constant values -- hoist above the loop
  • extra_kwargs return type is unparameterized dict -- use -> dict[str, Any]
  • should_deploy_executable_commands should eventually live in integration/consent.py, not command_integrator
  • Internal allow_executable_commands: bool will not scale to two independent target consent flags -- track a follow-up for frozenset[str]

CLI Logging Expert

Nits:

  • Double-guard on verbose_detail at install.py:1604 -- remove outer if logger.verbose: since verbose_detail() self-guards
  • command(s) hedging in the consent-gate warn message -- use proper singular/plural

DevX UX Expert

Nits:

  • No env-var escape hatch (APM_ALLOW_CURSOR_COMMANDS=1) documented for CI pipelines
  • Lifecycle note embedded in flag bullet -- should be a > **Note:** admonition in the integration doc
  • Warning message text not quoted in docs -- add exact warning string as a code block
  • --allow-cursor-commands interaction with --target is undocumented

Supply Chain Security Expert

Nits:

  • allowed-tools passthrough to Cursor is unvalidated; add a comment noting its Claude-specific semantics should be reviewed when a dedicated cursor_command formatter lands

Auth Expert

Inactive -- No auth files changed; PR adds Cursor command deployment gate (--allow-cursor-commands) with no changes to AuthResolver, token management, credential resolution, or host classification.

OSS Growth Hacker

Required:

  • CHANGELOG hook buries the value proposition under 150 words of implementation noise -- rewrite to lead with the user-outcome sentence
  • Lifecycle deprecation warning buried as the last clause of the last sentence -- promote to a prominent callout immediately after the hook

Nits:

  • what-is-apm.md zero-lock-in section does not list .cursor/commands/ even though the target comparison table now does
  • Security framing ("APM treats prompt deployment like npm treats scripts") is the actual story angle but absent from every user-facing surface -- use it in the release post
  • Cursor commands feature not surfaced in README hero or quick-start

Verdict computed deterministically: 2 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.

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 #1046 · ● 2.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
@danielmeppiel danielmeppiel removed the panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. label Apr 30, 2026
danielmeppiel and others added 2 commits May 1, 2026 01:22
…fault

Cursor slash commands have identical invocation semantics to
Claude/OpenCode/Gemini -- the user must type /cmdname to invoke. They
do not auto-execute on disk-write or IDE startup, so the npm
--ignore-scripts / Threat microsoft#8 framing was incorrect. The gate created
artificial asymmetry between targets with identical UX and violated
portable-by-manifest.

Removed:
- --allow-cursor-commands CLI flag
- TargetProfile.requires_executable_consent field
- should_deploy_executable_commands() helper + gate block
- IntegrateOptions per-primitive routing (only consumer was the gate)
- allow_executable_commands plumbing through 7 layers
- Threat microsoft#8 framing from docs + CHANGELOG

Kept:
- Cursor commands PrimitiveMapping (the actual feature)
- Lossy-frontmatter diagnostics.warn() per file (real value: surfaces
  dropped Cursor-specific keys to package authors)
- Cursor 1.6+ lifecycle note (Cursor de-emphasizing commands)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Architecture is sound and the feature direction is correct, but six required findings -- two output-flooding anti-patterns, one security severity miscategorization, and one performance/documentation contradiction -- must be resolved before merge.

Required before merge (6 items)

  • [python-architect] pkg_resolved = package_path.resolve() is re-evaluated inside the per-file loop, directly contradicting the inline comment that claims the check is "loop-fast" at src/apm_cli/integration/base_integrator.py

    • Why: Path.resolve() is a syscall (stat + readlink chain). Computing it O(n) times instead of O(1) contradicts the documented intent and misleads future contributors in a security-adjacent path.
    • Suggested fix: Hoist pkg_resolved = package_path.resolve() to immediately before the for f in files: loop and delete the misleading comment.
  • [cli-logging-expert] Dropped-keys warn() fires once per command file, flooding the terminal for multi-file packages at src/apm_cli/integration/command_integrator.py

    • Why: The Signal-to-noise rule requires every message to pass the "So What?" test. A package shipping 10 prompt files with Cursor-specific frontmatter emits 10 identical warnings. The set of dropped keys is the same across files; one consolidated summary is sufficient and actionable.
    • Suggested fix: Collect dropped-key occurrences across all files for a given target and emit one consolidated warn() per (target, package): e.g., "Cursor commands: dropped frontmatter keys [author, mcp] in 4 files. Only description, allowed-tools, model, argument-hint, input are preserved."
  • [cli-logging-expert] One-shot passthrough notice leaks internal implementation detail ("Claude-compatible frontmatter") to the end user at src/apm_cli/integration/command_integrator.py

    • Why: A Cursor user has no reason to know the shared transformer is Claude's. The message describes a non-action the tool took and violates the "no internal jargon" rule. It also conflates a temporary TODO workaround with a permanent user-facing feature.
    • Suggested fix: Either drop this notice entirely (the per-file dropped-keys warning already covers the lossy-transform signal), or rephrase to an outcome-only info() emitted only in --verbose mode: "Cursor commands written with shared frontmatter fields (description, allowed-tools, model, argument-hint, input)."
  • [devx-ux-expert] Per-file diagnostics.warn() for dropped frontmatter keys floods the terminal on a successful install at src/apm_cli/integration/command_integrator.py (line ~299)

    • Why: Warnings on a clean install are an anti-pattern. A package with 5 prompt files each containing author, mcp, parameters generates 5 warnings during normal operation, making the install look broken to users.
    • Suggested fix: Apply the same per-install-run deduplication pattern already used by _should_emit_passthrough_notice. Aggregate dropped keys across all files per package and emit one warn() per (target, package). Per-file detail should be info() behind --verbose only.
  • [devx-ux-expert] Skip notice ("create a .cursor/ directory to enable...") fires once per installed package, not once per install run, at src/apm_cli/integration/command_integrator.py (line ~441)

    • Why: A user with 10 packages in apm.lock and no Cursor installed sees 10 identical skip notices on every apm install. This is the terminal-flooding anti-pattern applied to the non-Cursor path. Users without Cursor are penalized on every run.
    • Suggested fix: Add _skip_notified: set[str] on the integrator instance (same pattern as _passthrough_notified). Emit the skip notice once per (target, install run).
  • [supply-chain-security-expert] PathTraversalError rejections are emitted as diagnostics.warn() rather than diagnostics.error(), making traversal attempts invisible to CI policy gates at src/apm_cli/integration/command_integrator.py (line ~500)

    • Why: A malicious package shipping ../../evil.prompt.md is correctly rejected at runtime, but the rejection event is swallowed as a warning. Any CI gate or policy filter that treats only errors as blocking will silently pass an install that just rejected a supply-chain attack attempt. Traversal rejection is an integrity event, not a degraded-quality event.
    • Suggested fix: Change both PathTraversalError catch blocks in integrate_commands_for_target from diagnostics.warn() to diagnostics.error() (or a dedicated security-severity level if one exists). If DiagnosticCollector lacks an error level, add one and treat it as blocking in the CI gate.

Nits (17 items, skip if you want)

  • [python-architect] _transform_prompt_to_command returns a 4-tuple -- a _TransformResult NamedTuple would be safer and self-documenting, especially when the cursor_command format lands and the tuple grows again
  • [python-architect] _PRESERVED_COMMAND_KEYS encodes transformer knowledge inside the integrator layer; it belongs adjacent to the claude_command transformer so the Cursor refactor only touches one place
  • [python-architect] _should_emit_passthrough_notice is named like a pure predicate but mutates self._passthrough_notified as a side-effect; consider renaming to _check_and_mark_passthrough_notice
  • [python-architect] target_name: str = "claude" default risks silent Claude-branding if a future caller forgets the kwarg; consider "unknown" sentinel or making it required
  • [cli-logging-expert] Skip notice should lead with the action: "To enable Cursor command deployment, create a .cursor/ directory." (fix first, context second)
  • [cli-logging-expert] "frontmatter keys not written by the shared command transformer" is internal jargon; prefer "not supported for {target_name} commands"
  • [cli-logging-expert] Arguments mapping message uses raw -> arrow; prefer plain "to": "Mapped input to command arguments in {target.name}"
  • [cli-logging-expert] Pluralization: use singular "key" vs plural "keys" based on len(dropped_keys)
  • [devx-ux-expert] CHANGELOG lifecycle warning ("Cursor is de-emphasizing commands...") is buried mid-sentence in a dense paragraph -- break out as a separate bulleted sub-item or bold "Note:" callout
  • [devx-ux-expert] cli-commands.md install table and ide-tool-integration.md have inconsistent lifecycle warning signal; mirror at least the "monitor Cursor release notes" link in cli-commands.md
  • [supply-chain-security-expert] pkg_resolved recomputed per loop iteration in _collect_files -- hoist above the for f in sorted(...) loop to avoid redundant syscalls on large packages
  • [supply-chain-security-expert] TODO(cursor-command-format) appears in three separate places; a single tracking issue number would make the debt easier to grep and close
  • [oss-growth-hacker] CHANGELOG entry leads with the win ("out of the box") and buries the deprecation caveat -- reorder so the lifecycle note is a distinct parenthetical or sub-item, not a trailing Note: sentence most skimmers miss
  • [oss-growth-hacker] "Create a .cursor/ directory" is a non-obvious opt-in step for Cursor-native users; add one sentence in ide-tool-integration.md naming which Cursor Settings action creates .cursor/ so users do not resort to manual mkdir
  • [oss-growth-hacker] First-run Cursor-authored packages with many commands will see per-file dropped-key warnings that look like errors -- the aggregate-warning fix above resolves this too
  • [oss-growth-hacker] No "What's New" callout or release story angle published alongside this feature; draft a release post beat or add a "What's new" callout block to the Cursor integration docs section

CEO arbitration

This PR is REJECTED on six required findings spanning performance, output quality, and security severity. The panel reached strong consensus on two of the six: cli-logging-expert and devx-ux-expert independently and identically flagged the per-file dropped-keys warn() as a terminal-flooding anti-pattern. The fix direction is also agreed -- aggregate dropped-key occurrences per target or per package and emit a single consolidated warning. No dissent to resolve there; both findings stand and compound each other. The devx-ux-expert additionally flagged the skip-notice firing once per package rather than once per install run, which creates a symmetric flooding problem on the non-Cursor path. Both output-path findings must be addressed together before merge. The security finding from supply-chain-security-expert is unambiguous and independent: PathTraversalError rejections are emitted as warn() rather than error(), which renders them invisible to any CI policy gate that filters on severity. A traversal rejection is an integrity event; it must surface as an error. This is a one-line change with no ergonomic cost and no trade-off to weigh. The python-architect required finding on pkg_resolved being recomputed inside the per-file loop is defensible at required level because the inline comment explicitly claims "loop-fast" while doing the opposite -- the comment actively misleads future contributors.

Dissent resolved: python-architect elevated pkg_resolved-per-iteration to required; supply-chain-security-expert left it as a nit. CEO sides with the architect. The deciding factor is not the O(n) syscall cost -- that is minor at realistic package sizes -- but the "loop-fast" comment that asserts the opposite of what the code does. A misleading comment in a security-adjacent path is a correctness defect, not a style nit. The fix is a one-line hoist and a comment deletion; it should ship with this PR.

Growth/positioning note: OSS Growth Hacker timing verdict: ship. Cursor has not removed slash commands; the feature is gated on .cursor/ presence so blast radius is minimal and rollback is trivial. The conversion gap is the first-run opt-in story -- creating .cursor/ is a non-obvious step for Cursor-native users who expect zero-config setup. The CHANGELOG and docs should name the Cursor Settings path that creates .cursor/ rather than leaving it as a raw mkdir instruction. This is a one-paragraph doc fix that materially reduces first-run friction for the Cursor user segment.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class BaseIntegrator {
        <<TemplateMethod>>
        +find_files_by_glob(pkg, glob, subdirs) list
        +check_collision(rel, target, pkg) bool
        +validate_deploy_path(path) Path
        +resolve_links(content, src, tgt) tuple
    }
    note for BaseIntegrator "Template Method: subclasses implement\nintegrate_X; base owns collision,\nmanifest-sync, path-security"

    class CommandIntegrator {
        <<ConcreteTemplate>>
        -_passthrough_notified set
        +find_prompt_files(pkg) list
        +integrate_command(src, tgt, info, orig, diag, target_name) int
        +integrate_commands_for_target(pkg, root, target, diag) IntegrationResult
        -_transform_prompt_to_command(src) tuple
        -_should_emit_passthrough_notice(name, fmt_id) bool
    }

    class PromptIntegrator {
        <<ConcreteTemplate>>
        +integrate_prompt(src, tgt, info, diag) int
    }

    class AgentIntegrator {
        <<ConcreteTemplate>>
    }

    class TargetProfile {
        <<ValueObject>>
        +name str
        +root_dir str
        +auto_create bool
        +primitives dict
        +for_scope(user_scope) TargetProfile
    }

    class PrimitiveMapping {
        <<ValueObject>>
        +subdir str
        +extension str
        +format_id str
        +deploy_root str
    }

    class IntegrationResult {
        <<ValueObject>>
        +files_linked int
        +files_skipped int
        +collisions int
        +warnings list
        +links_resolved int
    }

    class DiagnosticCollector {
        <<CollectThenRender>>
        +info(message, package, detail)
        +warn(message, package)
        +render_summary()
    }
    note for DiagnosticCollector "Collect-then-render: push during\noperation, render at install end"

    class PathTraversalError {
        <<Exception>>
    }

    BaseIntegrator <|-- CommandIntegrator
    BaseIntegrator <|-- PromptIntegrator
    BaseIntegrator <|-- AgentIntegrator
    CommandIntegrator ..> TargetProfile : reads
    CommandIntegrator ..> PrimitiveMapping : reads
    CommandIntegrator ..> IntegrationResult : returns
    CommandIntegrator ..> DiagnosticCollector : pushes to
    CommandIntegrator ..> PathTraversalError : catches
    TargetProfile *-- PrimitiveMapping : primitives

    class CommandIntegrator:::touched
    class BaseIntegrator:::touched
    class PrimitiveMapping:::touched
    class TargetProfile:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install]) --> B[_integrate_package_primitives\ninstall.py]
    B --> C{cursor in targets?}
    C -- no --> Z([skip])
    C -- yes --> D{.cursor/ dir exists?\ntarget.auto_create=False}
    D -- missing --> E[diagnostics.info: Skipped .cursor/commands/\nonce per install run AFTER fix]
    E --> Z
    D -- present --> F[find_prompt_files\nbase_integrator.find_files_by_glob]
    F --> G["[FS] glob *.prompt.md in .apm/prompts/"]
    G --> H{is_symlink? skip}
    H --> I["[FS] resolved = f.resolve()\npkg_resolved = package_path.resolve()\n(WARNING: called per-file, should be hoisted)"]
    I --> J{resolved.is_relative_to\npkg_resolved?}
    J -- no --> K[skip hardlink escape]
    J -- yes --> L{_should_emit_passthrough_notice}
    L -- yes --> M[diagnostics.info]
    L -- no --> N
    M --> N[validate_path_segments]
    N -- PathTraversalError --> O["diagnostics.error (AFTER FIX)\nfiles_skipped++"]
    N -- ok --> P[ensure_path_within]
    P -- PathTraversalError --> Q["diagnostics.error (AFTER FIX)\nfiles_skipped++"]
    P -- ok --> R[check_collision]
    R -- collision --> S[diagnostics.warn]
    R -- clear --> T[_transform_prompt_to_command]
    T --> U["(command_name, post, warnings, dropped_keys)"]
    U --> V{dropped_keys?}
    V -- yes --> W["diagnostics.warn AGGREGATED (AFTER FIX)\none warn per target+package"]
    V -- no --> X
    W --> X{arguments mapped?}
    X -- yes --> Y[diagnostics.info]
    X -- no --> AA
    Y --> AA["write .cursor/commands/<name>.md"]
    AA --> BB[files_linked++]
    BB --> CC([IntegrationResult])
Loading

Design patterns: Template Method -- BaseIntegrator owns the file-loop skeleton; CommandIntegrator supplies integrate_command as the leaf step. Collect-then-render -- DiagnosticCollector accumulates warn/info calls throughout and renders at install completion. Pragmatic suggestion: _TransformResult NamedTuple for _transform_prompt_to_command's return value -- the 4-tuple will grow again when cursor_command format lands; a named type costs two lines and eliminates positional indexing errors at all three call sites.

Required: 1 (pkg_resolved per-loop, see above)
Nits: 4 (NamedTuple, key placement, method naming, default sentinel)

CLI Logging Expert

Required: 2 (per-file dropped-keys flood, passthrough notice leaks jargon)
Nits: 4 (skip notice phrasing, jargon, arrow vs. "to", pluralization)

DevX UX Expert

Required: 2 (per-file warn floods terminal, skip notice fires per package)
Nits: 3 (target_name default, CHANGELOG lifecycle caveat buried, doc inconsistency)

Supply Chain Security Expert

Required: 1 (traversal rejections must be error, not warn)
Nits: 2 actionable (pkg_resolved hoist, TODO issue tracking) + 3 confirmatory (frontmatter injection mitigated, hardlink guard correct, .cursor/ risk comparable to .claude/)

Auth Expert

Inactive -- No auth-related files changed; PR adds Cursor command deployment to command_integrator.py and targets.py with no effect on AuthResolver, token precedence, credential resolution, or HTTP authorization headers.

OSS Growth Hacker

Required: None.
Nits: 4 (CHANGELOG caveat ordering, .cursor/ opt-in clarity, dropped-keys noise, story angle / release callout)

Side-channel: Timing is acceptable -- Cursor has not removed commands; feature is gated on .cursor/ presence (low blast radius). Conversion gap is the first-run opt-in story. "Same slash-command UX as Claude/OpenCode/Gemini" is technically accurate but the lossy frontmatter transform is real -- the per-file warn (once fixed to aggregate) is the right mitigation.

Verdict computed deterministically: 6 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.

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 #1046 · ● 2.6M ·

@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
@stbenjam
Copy link
Copy Markdown
Contributor Author

stbenjam commented May 1, 2026

I am confused by this:

Suggested fix: Surface a distinct install-time warning that commands deployed to .cursor/commands/ will be executable as Cursor slash commands. Consider requiring an explicit opt-in flag (e.g. --allow-commands) or a manifest field in apm.yml to enable command deployment.

All coding agents (claude, gemini, copilot, etc) take / commands, and they get deployed without any opt-in flag? Why is cursor special? Cursor commands are not different than anybody else.

@stbenjam
Copy link
Copy Markdown
Contributor Author

stbenjam commented May 1, 2026

Nevermind, I see the latest commit now drops this - 6adb357

@danielmeppiel
Copy link
Copy Markdown
Collaborator

@stbenjam I am refactoring the review panel, it will now show as advisory, not a gate. We are instrumenting this repo heavily with agents, but that's a learning process. I'll review this and merge soon. Thanks!

@danielmeppiel danielmeppiel removed the panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. label May 2, 2026
Copilot and others added 2 commits May 2, 2026 18:33
# Conflicts:
#	docs/src/content/docs/integrations/ide-tool-integration.md
#	src/apm_cli/integration/targets.py
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label May 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

APM Review Panel: ship_with_followups

PR #1046 delivers first-class Cursor 1.6+ slash command parity for apm install -- a high-value surface win for Cursor's 4M+ users that is nearly ready to ship, pending one blocking doc typo fix and a hardlink-guard test gap on the security surface.

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

The panel converged cleanly on four actionable clusters. The single hard blocker is the doc-writer's finding: security.md misspells the OpenCode deployment path as .opencode/command/*.md (missing trailing s). This is a factual error in a security-scoped doc that will be read by enterprise evaluators -- fix before merge, no exceptions.

Two panelists (cli-logging-expert and devx-ux-expert) independently converge on the same two diagnostic message defects: (1) the passthrough notice fires unconditionally even when zero frontmatter keys are dropped, generating noise on every clean Cursor install; (2) the dropped-keys warning uses the phrase "shared command transformer", which is machine-internal jargon invisible to package authors. Both fixes are low-effort, high-signal UX corrections that should land in this PR. The supply-chain-security recommendation to upgrade the post-transform scan from WARN_POLICY to BLOCK_POLICY is architecturally sound and consistent with APM's secure-by-default principle -- the primary gate (pre-install BLOCK_POLICY) is unaffected, but the defense-in-depth gap is real and cheap to close. The test-coverage panel flagged a missing unit test for the new hardlink containment guard in base_integrator.py; because this guard is on the secure-by-default surface, the missing test inherits near-blocking weight per the panel's evidence-weighting rules, even though the persona classified it recommended. The author should add the escape-path branch test before merge or immediately after in a fast-follow.

On the growth side, the CHANGELOG messaging is adoption-chilling: it leads the feature announcement with a lifecycle caveat ("Cursor is de-emphasizing commands") that belongs in reference docs, not in the first sentence of a feature entry. Rewrite the CHANGELOG entry and add one line to the README features list -- these are two-minute changes that meaningfully improve top-of-funnel discoverability for a 4M-user audience.

Dissent. No direct contradictions between panelists. The test-coverage-expert's test_full_dispatch_deploys_to_cursor finding carries outcome: unknown because pytest was unavailable in the review environment; per CEO weighting rules this is downgraded to opinion-only. Auth-expert correctly marked themselves inactive.

Aligned with: Portable by manifest (same .prompt.md files, no Cursor-specific format required). Secure by default (primary gate unaffected; two open items: WARN_POLICY gap and missing hardlink-escape unit test). Governed by policy (dropped-keys warning and passthrough notice must be legible to package authors). Multi-harness (same manifest, new target, shared transformer -- architecture holds). OSS community driven (CHANGELOG and README gaps leave the feature undiscoverable). Pragmatic as npm (auto-deploys when .cursor/ exists, zero extra config).

Growth signal. Cursor's 4M+ user base represents APM's largest single-ecosystem expansion to date. The micro-launch beat -- "Cursor users: apm install now auto-deploys slash commands" -- is the right one-liner. Two concrete actions unlock it: (1) rewrite the CHANGELOG entry to lead with the user benefit, not the lifecycle caveat; (2) add one sentence to the README feature list. Cross-tool parity (Claude Code, Cursor, OpenCode, Copilot) is APM's core defensibility claim -- this PR advances it, and the messaging should say so loudly.

Panel summary

Persona B R N Takeaway
Python Architect 0 3 2 Solid Cursor command dispatch using the data-driven target pattern. Two minor design gaps: identity alias in _BUCKET_ALIASES and stale class docstring; no blockers.
CLI Logging Expert 0 2 2 Two signal-to-noise issues: one-shot passthrough notice fires unconditionally (noise when no keys dropped); dropped-keys warn uses internal jargon.
DevX UX Expert 0 2 2 Happy-path noise and internal jargon in two diagnostic messages; commands.md skill not synced; feature shape and docs coverage otherwise solid.
Supply Chain Security Expert 0 1 1 No path-traversal or post-install-execution bypasses found; one defense-in-depth gap in post-transform scan policy; one nit on allowed-tools passthrough.
OSS Growth Hacker 0 3 1 Solid parity win for Cursor's 4M+ users, but CHANGELOG leads with a deprecation caveat and the feature stays invisible to top-of-funnel visitors.
Doc Writer 1 2 1 One blocking typo (opencode path), one recommended restructure (security.md placement), one recommended cell verbosity issue, one nit on duplication.
Test Coverage Expert 0 2 1 Cross-module integration surface has fixtures-tier test in unit/ folder (not run); hardlink containment guard in base_integrator.py has no test; all other surfaces 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) Fix .opencode/command/*.md -> .opencode/commands/*.md in security.md -- Factual error in a security-scoped enterprise doc. One character, zero ambiguity. Must fix before merge.
  2. [Test Coverage Expert] Add unit test for hardlink containment escape-path branch in base_integrator.py -- New security guard with no automated coverage on the escape path. Missing test on a secure-by-default surface inherits near-blocking weight per panel rules.
  3. [CLI Logging Expert] Gate passthrough notice on actual dropped keys; rewrite dropped-keys warning to remove "shared command transformer" jargon -- Two converging panelists (cli-logging + devx-ux) on the same two issues. Low-effort, high-signal UX fix that reduces terminal noise and makes policy failures legible to package authors.
  4. [Supply Chain Security Expert] Upgrade post-transform content scan from WARN_POLICY to BLOCK_POLICY in integrate_command() -- Defense-in-depth gap: transform-introduced content currently deploys even on critical scan findings. Cheap fix; consistent with secure-by-default principle.
  5. [OSS Growth Hacker] Rewrite CHANGELOG entry user-first; add one README line about slash command auto-deployment -- Current CHANGELOG leads with a deprecation caveat that chills adoption. README is silent on slash commands. Two-minute fix unlocks top-of-funnel discoverability for 4M+ Cursor users.

Architecture

classDiagram
    direction LR

    class BaseIntegrator {
        <<Abstract>>
        +link_resolver: UnifiedLinkResolver
        +should_integrate(project_root) bool
        +check_collision(target_path, rel_path, managed_files, force) bool
        +validate_deploy_path(rel_path, project_root) bool
        +partition_managed_files(managed_files, targets) dict
        +sync_remove_files(project_root, managed_files, prefix) dict
        +find_files_by_glob(package_path, pattern, subdirs) list
        +init_link_resolver(package_info, project_root)
        +resolve_links(content, source, target) tuple
        -_BUCKET_ALIASES: dict
    }

    class CommandIntegrator {
        <<ConcreteIntegrator>>
        -_passthrough_notified: set
        +find_prompt_files(package_path) list
        +integrate_command(source, target, package_info, original_path, diagnostics, target_name) int
        +integrate_commands_for_target(target, package_info, project_root, force, managed_files, diagnostics) IntegrationResult
        +sync_for_target(target, apm_package, project_root, managed_files) dict
        -_transform_prompt_to_command(source) tuple
        -_should_emit_passthrough_notice(target_name, format_id) bool
        -_write_gemini_command(source, target)
    }

    class IntegrationResult {
        <<ValueObject>>
        +files_integrated: int
        +files_updated: int
        +files_skipped: int
        +target_paths: list
        +links_resolved: int
    }

    class TargetProfile {
        <<ValueObject>>
        +name: str
        +root_dir: str
        +primitives: dict
        +auto_create: bool
        +detect_by_dir: bool
        +for_scope(user_scope) TargetProfile
        +deploy_path(project_root) Path
        +supports(primitive) bool
    }

    class PrimitiveMapping {
        <<ValueObject>>
        +subdir: str
        +extension: str
        +format_id: str
        +deploy_root: str
    }

    class SecurityGate {
        <<IOBoundary>>
        +scan_text(text, path, policy) ScanVerdict
    }

    class DiagnosticCollector {
        <<Observer>>
        +warn(message, package)
        +info(message, package, detail)
        +security(message, package, detail, severity)
        +skip(rel_path)
    }

    note for CommandIntegrator "Template Method: integrate_commands_for_target\nis the fixed skeleton; _transform_prompt_to_command\nand _write_gemini_command are hot-swap steps.\nData-driven dispatch via format_id selects writer."
    note for TargetProfile "cursor: commands PrimitiveMapping\nuses format_id='claude_command'\n(shared transformer, TODO cursor_command)"

    BaseIntegrator <|-- CommandIntegrator
    CommandIntegrator ..> IntegrationResult : returns
    CommandIntegrator ..> TargetProfile : reads
    CommandIntegrator ..> DiagnosticCollector : pushes to
    CommandIntegrator ..> SecurityGate : calls
    TargetProfile *-- PrimitiveMapping : contains

    class CommandIntegrator:::touched
    class TargetProfile:::touched
    class BaseIntegrator:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install --target cursor]) --> B[resolve_targets / active_targets]
    B --> C{.cursor/ dir exists?}
    C -- No, auto_create=False --> D[diagnostics.info: skipped .cursor/commands/]
    C -- Yes --> E[integrate_commands_for_target\ncommand_integrator.py]
    E --> F[_should_emit_passthrough_notice\ntarget=cursor, format_id=claude_command]
    F -- first call --> G[diagnostics.info: Claude-compatible\nfrontmatter intentional]
    F -- already notified --> H
    G --> H[find_prompt_files\nbase_integrator.find_files_by_glob]
    H --> I[FS: glob *.prompt.md in install_path/.apm/prompts]
    I --> J{symlink? skip\nhardlink outside pkg? skip\ncontainment guard}
    J --> K[for each prompt_file]
    K --> L[validate_path_segments base_name\npath_security.py]
    L -- PathTraversalError --> M[diagnostics.warn + skip]
    L -- OK --> N[ensure_path_within target_path, commands_dir\npath_security.py]
    N -- PathTraversalError --> M
    N -- OK --> O[check_collision\nbase_integrator.py]
    O -- collision+no force --> P[diagnostics.skip]
    O -- OK --> Q{format_id?}
    Q -- gemini_command --> R[FS: _write_gemini_command]
    Q -- claude_command\ncursor/claude/opencode --> S[integrate_command\ntarget_name=cursor]
    S --> T[_transform_prompt_to_command\nparse frontmatter, map keys]
    T --> U[dropped_keys = source_keys - _PRESERVED_COMMAND_KEYS]
    U --> V{dropped_keys?}
    V -- yes --> W[diagnostics.warn: frontmatter keys not written]
    V --> X[resolve_links via UnifiedLinkResolver]
    X --> Y[SecurityGate.scan_text compiled output]
    Y --> Z[FS: target.parent.mkdir + write .cursor/commands/name.md]
    Z --> AA[IntegrationResult files_integrated++]
Loading

Recommendation

Fix the one-character OpenCode path typo in security.md before merge -- it is a factual error in an enterprise-facing doc. The remaining items (hardlink test, diagnostic message rewrites, BLOCK_POLICY upgrade, CHANGELOG rewrite) are high-signal but non-blocking; the PR author should resolve them in this PR if turnaround is fast, or commit to fast-follow PRs if not. The feature architecture is sound, the security primary gate is unaffected, and the Cursor parity win is strategically significant enough to not delay further for opinion-tier findings.


Full per-persona findings

Python Architect

  • [recommended] Identity alias commands_cursor -> commands_cursor in _BUCKET_ALIASES is a no-op and misleads readers at src/apm_cli/integration/base_integrator.py:183
    Every other entry in _BUCKET_ALIASES maps a raw key to a different canonical name (e.g. commands_claude -> commands). The new entry maps to itself -- the dict.get(raw, raw) fallback already handles it. Readers will assume this entry does something meaningful.
    Suggested: Remove 'commands_cursor': 'commands_cursor' from _BUCKET_ALIASES -- the dict.get(raw, raw) default already handles it. If explicit registration is desired for discoverability, add a comment to that effect.
  • [recommended] Class-level docstring of CommandIntegrator still says .claude/commands/ only at src/apm_cli/integration/command_integrator.py:127
    The class now handles any target. Stale single-target docstring will confuse the next contributor adding a fourth target.
    Suggested: Replace with: "Integrates .prompt.md files as slash commands for any target that declares a commands primitive (Claude Code, Cursor, OpenCode, Gemini CLI). Dispatch is data-driven via TargetProfile.primitives['commands'].format_id."
  • [recommended] integrate_commands_for_target existence-check uses target.root_dir but target_root may use mapping.deploy_root at src/apm_cli/integration/command_integrator.py:437
    The skip guard checks (project_root / target.root_dir).is_dir() but target_root uses mapping.deploy_root when set. Latent inconsistency for future targets using deploy_root for commands.
    Suggested: Change the guard to: if not target.auto_create and not target_root.is_dir():
  • [nit] _should_emit_passthrough_notice hardcodes the string 'claude' rather than referencing a constant at src/apm_cli/integration/command_integrator.py:150
    If the claude target slug is ever renamed, this guard silently stops suppressing the notice for the canonical Claude deploy path.
  • [nit] Design-patterns note: Template Method + data-driven dispatch via format_id is the right shape. No Strategy Protocol warranted at current scale.

CLI Logging Expert

  • [recommended] One-shot passthrough notice fires even when zero Cursor-specific keys are dropped at src/apm_cli/integration/command_integrator.py
    The _should_emit_passthrough_notice guard deduplicates per (target, run) but fires for every install touching a cursor-style target's commands, even when nothing is lossy. Fails the Signal-to-noise test -- pure noise for packages using only preserved keys.
    Suggested: Gate the passthrough notice on whether at least one file in the batch actually had dropped_keys, or drop the notice entirely -- the per-file dropped-keys warn already tells the story when relevant.
  • [recommended] Dropped-keys warn message uses internal implementation jargon at src/apm_cli/integration/command_integrator.py
    "frontmatter keys not written by the shared command transformer" -- "shared command transformer" is machine-internal language. Package authors don't know what this is.
    Suggested: Replace with: f"{target_name.capitalize()} command {command_name}: frontmatter keys not supported for {target_name} commands and were dropped: {', '.join(dropped_keys)}."
  • [nit] Dropped-keys warn appends a hardcoded key list that duplicates _PRESERVED_COMMAND_KEYS at src/apm_cli/integration/command_integrator.py
    The trailing "Only description, allowed-tools, model, argument-hint, and input are preserved." will silently go stale if _PRESERVED_COMMAND_KEYS changes.
  • [nit] One-shot passthrough notice is attributed to whichever package happens to be processed first at src/apm_cli/integration/command_integrator.py
    package=pkg_name on the one-shot notice ties a target-level message to an arbitrary package name, which can confuse DiagnosticCollector rendering in multi-package installs.
    Suggested: Pass package=None or package="" for the passthrough notice.

DevX UX Expert

  • [recommended] Passthrough notice fires unconditionally on every Cursor install, polluting quiet happy path at src/apm_cli/integration/command_integrator.py
    diagnostics.info() items always render non-verbose. Fires even when zero keys are dropped. Anti-pattern: output floods terminal on success.
    Suggested: Remove the passthrough notice entirely, or gate it on verbose / move to debug level.
  • [recommended] Dropped-keys warning uses internal implementation jargon invisible to package authors at src/apm_cli/integration/command_integrator.py
    "shared command transformer" is an internal class name. A package author writing YAML frontmatter has no mental model for "transformer".
    Suggested: Rewrite to: "{Target} command {name}: frontmatter keys dropped (unsupported for this target): {keys}. Supported: description, allowed-tools, model, argument-hint, input."
  • [nit] packages/apm-guide/.apm/skills/apm-usage/commands.md not updated at packages/apm-guide/.apm/skills/apm-usage/commands.md
    Shipped skill resource not synced with docs. Cursor users consulting the in-IDE reference will not discover the feature.
    Suggested: Add a row or note under apm install describing .cursor/commands/*.md deployment, mirroring the cli-commands.md addition.
  • [nit] Skip message loses "in your project root" anchor at src/apm_cli/integration/command_integrator.py
    Dropping the spatial anchor "in your project root" reduces orientation for first-run users.
    Suggested: Append "in your project root" to the skip message.

Supply Chain Security Expert

  • [recommended] Post-transform content scan uses WARN_POLICY -- critical hidden-char findings do not gate the write at src/apm_cli/integration/command_integrator.py
    integrate_command() calls SecurityGate.scan_text with WARN_POLICY (on_critical='warn'), then always writes the file. The pre-install BLOCK_POLICY gate remains the primary guard, but using WARN_POLICY here means a transform-introduced critical finding would silently deploy. Defense-in-depth gap; cheap to close.
    Suggested: Replace policy=WARN_POLICY with policy=BLOCK_POLICY, or add: if scan_verdict and scan_verdict.has_critical: files_skipped += 1; continue before the write.
  • [nit] allowed-tools list entries passed verbatim without per-entry validation at src/apm_cli/integration/command_integrator.py
    Low risk in current trust model (users install trusted packages). Track as follow-up: surface resolved allowed-tools list via diagnostics.info() at install time so users see what permissions a command claims.

OSS Growth Hacker

  • [recommended] CHANGELOG entry opens the feature and immediately warns it may be going away
    Embedding "Cursor is de-emphasizing commands in favor of rules/skills -- monitor Cursor release notes" inside the same sentence that announces the feature is adoption-chilling.
    Suggested: Trim CHANGELOG to the user benefit ("Cursor users now get slash commands on apm install") and move the lifecycle caveat to a follow-up sentence or footnote.
  • [recommended] README and quickstart are silent about slash commands; feature is invisible to top-of-funnel
    README mentions Cursor 4 times but never mentions slash commands. Cursor users who land via search or word-of-mouth will not discover this feature.
    Suggested: Add one line to the README features list (e.g., "slash commands deployed to .claude/, .cursor/, .opencode/, .gemini/ automatically") and one sentence in the quickstart output example.
  • [recommended] CHANGELOG entry is written for maintainers, not users
    Mentions "diagnostics.warn() per file", "lossy transform", "Cursor-specific keys (author, mcp, parameters, ...) are dropped" -- implementation details that erode user confidence.
    Suggested: Rewrite user-first: "Cursor users get slash commands automatically when .cursor/ exists -- no extra steps."
  • [nit] No runnable proof-of-concept in any doc
    A single shell snippet (apm install <pkg> && ls .cursor/commands/) would make the feature tangible and shareable.

Auth Expert -- inactive

No auth files changed; PR deploys local .prompt.md files to .cursor/commands/ with no auth, token, or credential-resolution surface touched.

Doc Writer

  • [blocking] OpenCode path in security.md table is .opencode/command/*.md (missing trailing 's') at docs/src/content/docs/enterprise/security.md
    All other sources (source code, how-it-works.md, what-is-apm.md, package-authoring.md) use .opencode/commands/*.md. This is a factually wrong path that will mislead readers.
    Suggested: Change .opencode/command/*.md -> .opencode/commands/*.md in the slash command deployment table.
  • [recommended] Slash command deployment section belongs in ide-tool-integration.md, not security.md at docs/src/content/docs/enterprise/security.md
    security.md is scoped to supply chain security, content scanning, path safety, and trust models. The new section describes a deployment feature. The only security-relevant claim (commands are not auto-invoked) is one sentence.
    Suggested: Remove the "Slash command deployment" section from security.md. Add one sentence under the "File presence IS execution" paragraph. The full deployment table already lives in ide-tool-integration.md.
  • [recommended] New .cursor/commands/*.md table cell is too verbose, breaking visual consistency at docs/src/content/docs/integrations/ide-tool-integration.md
    Every other row in the Cursor integration table is <=10 words. The new row is a multi-sentence paragraph. Also duplicates content already in security.md.
    Suggested: Shorten cell to: "Slash commands from installed packages (.prompt.md files). Cursor 1.6+ only." Add a callout below the table for the lifecycle note.
  • [nit] Dropped-keys detail (author, mcp, parameters) duplicated across security.md and ide-tool-integration.md at docs/src/content/docs/enterprise/security.md
    Per "state once, reference elsewhere" rule. ide-tool-integration.md should be canonical.

Test Coverage Expert

  • [recommended] test_full_dispatch_deploys_to_cursor cannot be certified (pytest unavailable in review environment) at tests/unit/integration/test_command_integrator.py
    The test reads correctly and structurally proves the promise, but the outcome cannot be certified as passed per S7 PROBE RULE. Tier: integration-with-fixtures. Test: TestCursorCommandEndToEnd::test_full_dispatch_deploys_to_cursor. Proves: apm install deploys .prompt.md files to .cursor/commands/ via full dispatch pipeline with correct frontmatter. Proof (unknown): environment did not support running pytest.
  • [recommended] New hardlink containment guard in base_integrator.py has no test for the escape-path branch at tests/unit/integration/test_base_integrator.py
    Grepped tests/unit/integration/test_base_integrator.py for 'hardlink|containment|resolve.*relative' -- zero hits. The is_relative_to escape branch is untested. This is a secure-by-default surface. Proof (missing at unit tier): tests/unit/integration/test_base_integrator.py::test_collect_source_files_rejects_hardlink_escaping_package_root -- proves: a package shipping a hardlink pointing outside its install directory cannot be deployed. [secure-by-default, governed-by-policy]
  • [nit] Unit coverage for TestCursorCommandIntegration and TestCursorCommandPanelFindings is structurally sound and well-targeted at tests/unit/integration/test_command_integrator.py
    All six TestCursorCommandIntegration tests and four TestCursorCommandPanelFindings tests present and covering the new surface. TestCursorCommandPanelFindings::test_path_traversal_filename_rejected proves: a package shipping a traversal-laden filename is rejected and nothing is written to .cursor/commands/. [secure-by-default]

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.

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 #1046 · ● 7.2M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label May 2, 2026
Daniel Meppiel and others added 2 commits May 3, 2026 11:13
Apply the actionable findings from the apm-review-panel critique on
PR microsoft#1046:

* docs: fix `.opencode/command/*.md` -> `.opencode/commands/*.md`
  typo in the security guide (factual error in IDE integration map).

* CHANGELOG: rewrite Cursor entry user-first; lifecycle caveat moves
  to a trailing sentence so the value prop reads first.

* integrator: gate the one-shot 'cross-tool compatibility' notice on
  whether at least one file in the batch had dropped frontmatter
  keys.  Previously fired on every Cursor install -> noise on the
  happy path.  Cursor installs whose prompts only use the cross-tool
  subset now emit zero notice.  Other targets (claude, opencode)
  unaffected.

* integrator: rewrite the dropped-keys warn message to drop internal
  jargon ('shared command transformer'); use target-name framing
  ('not supported for cursor commands') and surface the canonical
  kebab-case key list (no camelCase aliases).

* integrator: upgrade post-transform scan_text from WARN_POLICY to
  BLOCK_POLICY.  When a critical finding is detected (e.g. a hidden
  tag char introduced by link resolution), skip the write and account
  for it in result.files_skipped.  Pre-install BLOCK gate already
  scans source files, so this is defense-in-depth for the
  transform-introduced edge case; no regression for legitimate
  packages.

* base_integrator: actually close the hardlink containment gap in
  find_files_by_glob.  The prior is_relative_to() check did NOT
  catch hardlinks -- a hardlink resolves to its own path inside the
  package root.  Add an st_nlink > 1 reject to prevent a malicious
  package shipping a hardlink to (e.g.) /etc/passwd from being read
  via integration.  Update the inline comment to be accurate about
  what each guard actually does.

Tests:

* tests/unit/integration/test_base_integrator.py: new
  test_hardlink_escaping_package_root_is_excluded covering the
  st_nlink reject; cleaned up F821 noqa via real pytest import.

* tests/unit/integration/test_command_integrator.py: 4 new tests in
  TestCursorCommandPanelFindings:
  - test_passthrough_notice_suppressed_on_clean_install
  - test_passthrough_notice_emitted_when_any_file_drops_keys
  - test_dropped_keys_warn_uses_user_facing_wording
  - test_critical_security_finding_blocks_write
  Updated the existing test_dropped_frontmatter_keys_warn assertion
  to match the new wording.

* tests/integration/test_marketplace_plugin_integration.py: 2 new
  integration tests locking in cursor 1.6+ command deployment via
  the install pipeline (deploy when .cursor/ exists; respect opt-in
  by NOT creating .cursor/ when missing).

Verification:
  uv run --extra dev ruff check src/ tests/        -> All checks passed!
  uv run --extra dev ruff format --check src/ tests/ -> 627 files OK
  uv run --extra dev pytest tests/unit ...         -> 7222 passed

Refs: microsoft#1046 (comment)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants