Repository navigation
fix(install): configure Cursor and OpenCode MCP servers at user scope - #3100
Alexei Vedernikov (alvedder) wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
OpenCode's incorrect schema key can cause an existing user-owned MCP entry to be overwritten.
Review effort: Balanced
Findings: 1
Open (4)
What changed in this PR
Enables user-scoped Cursor and OpenCode MCP configuration during global installs.
Changes:
- Adds scope-aware config paths, atomic writes, permissions, and safe parsing.
- Extends runtime discovery, stale cleanup, CLI help, and documentation.
- Adds global lifecycle, discovery, safety, and packaging tests.
| File | Description |
|---|---|
src/apm_cli/adapters/client/base.py |
Adds safe JSON read/write helpers. |
src/apm_cli/adapters/client/cursor.py |
Enables user-scoped Cursor MCP configuration. |
src/apm_cli/adapters/client/opencode.py |
Enables user-scoped OpenCode MCP configuration. |
src/apm_cli/integration/mcp_integrator.py |
Resolves scoped paths during cleanup. |
src/apm_cli/integration/mcp_integrator_install.py |
Updates discovery and supported-runtime messaging. |
src/apm_cli/commands/install.py |
Updates global-install help text. |
tests/unit/test_global_mcp_scope.py |
Tests scope, paths, safety, and cleanup. |
tests/unit/install/test_no_policy_flag.py |
Updates help-text expectations. |
tests/integration/test_global_cursor_opencode_mcp.py |
Tests global install and uninstall flows. |
packages/apm-guide/.apm/skills/apm-usage/commands.md |
Updates global MCP guidance. |
docs/src/content/docs/reference/cli/install.md |
Documents supported global targets. |
docs/src/content/docs/consumer/install-mcp-servers.md |
Documents config paths and safeguards. |
docs/src/content/docs/consumer/deploy-a-bundle.md |
Adds a global bundle example. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| supports_user_scope: bool = True | ||
| target_name: str = "opencode" | ||
| mcp_servers_key: str = "mcpServers" |
| | `apm install [PKGS...]` | Install APM, MCP, and LSP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)); one effective target decision drives package, MCP, and LSP phases; plain/frozen installs replay locked refs and cache state, while `--update` and `--refresh` require current upstream mutable refs; a successful non-dry-run install also reconciles deployed artifacts, lockfile ownership, and merge-hook config/sidecar entries for any target dropped from `targets:` | `--update` (deprecated; prefer `apm update`) refresh refs without accepting stale bare-cache answers, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite collisions and permit deployment after critical built-in scan findings (does NOT refresh refs by itself; `apm update --force` still requires upstream truth), `--frozen` CI-safe install that fails before any durable write when `apm.lock.yaml` is missing or out of sync with `apm.yml`, including MCP config state (mutually exclusive with `--update`, package additions, and `--mcp`; use normal install to create or repair lock state, then `apm audit` for SHA integrity), `--dry-run` (no package/deployment writes; positional packages and ref changes are previewed without changing an existing `apm.yml`; a newly bootstrapped manifest and explicit targets are kept), `--verbose`, `--only [apm\|mcp]` (APM packages or MCP/LSP service dependencies), `--target` (comma-separated, e.g. `--target claude,cursor`; resolution chain `--target` > apm.yml `targets:` > `apm config set target ...` > auto-detect; this decision is reused by package, MCP, and LSP phases; unresolved required service work fails non-zero before manifest or package writes, and native MCP/LSP write failures also fail non-zero; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` field so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `grok-build` for stable Grok Build rules, agents, commands, skills, and `AGENTS.md`; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `grok-cloud` after `apm experimental enable grok-cloud` to deploy skills only to `.grok/skills/`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Cursor, OpenCode, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--trust-bin` / `--no-trust-bin` (per-invocation consent for marketplace-plugin bin/ deployment: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ even if policy allows; default deploys with a warning), `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skills from a dependency that exposes selectable skills (repeatable; plugin manifests accept a leaf name or source-relative path under skills/; a CLI name or path that matches no declared skill fails before manifest or lockfile commit with available names; a stale persisted `skills:` pin that no longer matches an available source skill warns with the package, declared request names, and available names, and directs the user to edit `skills:` in apm.yml; persisted in apm.yml only on a successful CLI match; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry using that same effective target decision (the shared decision applies, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every explicitly selected target; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2). Explicit plugin component paths must resolve inside the plugin root; missing declarations fail before deployment and lockfile commit. | | ||
| | `apm install [PKGS...]` | Install APM, MCP, and LSP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)); one effective target decision drives package, MCP, and LSP phases; plain/frozen installs replay locked refs and cache state, while `--update` and `--refresh` require current upstream mutable refs; a successful non-dry-run install also reconciles deployed artifacts, lockfile ownership, and merge-hook config/sidecar entries for any target dropped from `targets:` | `--update` (deprecated; prefer `apm update`) refresh refs without accepting stale bare-cache answers, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite collisions and permit deployment after critical built-in scan findings (does NOT refresh refs by itself; `apm update --force` still requires upstream truth), `--frozen` CI-safe install that fails before any durable write when `apm.lock.yaml` is missing or out of sync with `apm.yml`, including MCP config state (mutually exclusive with `--update`, package additions, and `--mcp`; use normal install to create or repair lock state, then `apm audit` for SHA integrity), `--dry-run` (no package/deployment writes; positional packages and ref changes are previewed without changing an existing `apm.yml`; project bootstrap keeps its new manifest and explicit targets, while absent global `-g` bootstrap uses temporary preview state and does not create `~/.apm`), `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; resolution chain `--target` > apm.yml `targets:` > `apm config set target ...` > auto-detect; this decision is reused by package, MCP, and LSP phases; unresolved required service work fails non-zero before manifest or package writes, and native MCP/LSP write failures also fail non-zero; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` field so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `grok-build` for stable Grok Build rules, agents, commands, skills, and `AGENTS.md`; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `grok-cloud` after `apm experimental enable grok-cloud` to deploy skills only to `.grok/skills/`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Cursor, OpenCode, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--trust-bin` / `--no-trust-bin` (per-invocation consent for marketplace-plugin bin/ deployment: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ even if policy allows; default deploys with a warning), `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skills from a dependency that exposes selectable skills (repeatable; plugin manifests accept a leaf name or source-relative path under skills/; a CLI name or path that matches no declared skill fails before manifest or lockfile commit with available names; a stale persisted `skills:` pin that no longer matches an available source skill warns with the package, declared request names, and available names, and directs the user to edit `skills:` in apm.yml; persisted in apm.yml only on a successful CLI match; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry using that same effective target decision (the shared decision applies, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every explicitly selected target; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2). Explicit plugin component paths must resolve inside the plugin root; missing declarations fail before deployment and lockfile commit. | | ||
| | `apm install [PKGS...]` | Install APM, MCP, and LSP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)); one effective target decision drives package, MCP, and LSP phases; plain/frozen installs replay locked refs and cache state, while `--update` and `--refresh` require current upstream mutable refs; a successful non-dry-run install also reconciles deployed artifacts, lockfile ownership, and merge-hook config/sidecar entries for any target dropped from `targets:` | `--update` (deprecated; prefer `apm update`) refresh refs without accepting stale bare-cache answers, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite collisions and permit deployment after critical built-in scan findings (does NOT refresh refs by itself; `apm update --force` still requires upstream truth), `--frozen` CI-safe install that fails before any durable write when `apm.lock.yaml` is missing or out of sync with `apm.yml`, including MCP config state (mutually exclusive with `--update`, package additions, and `--mcp`; use normal install to create or repair lock state, then `apm audit` for SHA integrity), `--dry-run` (no package/deployment writes; positional packages and ref changes are previewed without changing an existing `apm.yml`; project bootstrap keeps its new manifest and explicit targets, while absent global `-g` bootstrap uses temporary preview state and does not create `~/.apm`), `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; resolution chain `--target` > apm.yml `targets:` > `apm config set target ...` > auto-detect; this decision is reused by package, MCP, and LSP phases; unresolved required service work fails non-zero before manifest or package writes, and native MCP/LSP write failures also fail non-zero; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` field so a later bare `apm update` reuses them, except absent global dry-runs only preview targets in temporary state; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `grok-build` for stable Grok Build rules, agents, commands, skills, and `AGENTS.md`; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `grok-cloud` after `apm experimental enable grok-cloud` to deploy skills only to `.grok/skills/`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Cursor, OpenCode, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--trust-bin` / `--no-trust-bin` (per-invocation consent for marketplace-plugin bin/ deployment: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ even if policy allows; default deploys with a warning), `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skills from a dependency that exposes selectable skills (repeatable; plugin manifests accept a leaf name or source-relative path under skills/; a CLI name or path that matches no declared skill fails before manifest or lockfile commit with available names; a stale persisted `skills:` pin that no longer matches an available source skill warns with the package, declared request names, and available names, and directs the user to edit `skills:` in apm.yml; persisted in apm.yml only on a successful CLI match; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry using that same effective target decision (the shared decision applies, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every explicitly selected target; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2). Explicit plugin component paths must resolve inside the plugin root; missing declarations fail before deployment and lockfile commit. | | ||
| | `apm install [PKGS...]` | Install APM, MCP, and LSP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)); one effective target decision drives package, MCP, and LSP phases; plain/frozen installs replay locked refs and cache state, while `--update` and `--refresh` require current upstream mutable refs; a successful non-dry-run install also reconciles deployed artifacts, lockfile ownership, and merge-hook config/sidecar entries for any target dropped from `targets:` | `--update` (deprecated; prefer `apm update`) refresh refs without accepting stale bare-cache answers, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite collisions and permit deployment after critical built-in scan findings (does NOT refresh refs by itself; `apm update --force` still requires upstream truth), `--frozen` CI-safe install that fails before any durable write when `apm.lock.yaml` is missing or out of sync with `apm.yml`, including MCP config state (mutually exclusive with `--update`, package additions, and `--mcp`; use normal install to create or repair lock state, then `apm audit` for SHA integrity), `--dry-run` (no package/deployment writes; positional packages and ref changes are previewed without changing an existing `apm.yml`; project bootstrap keeps its new manifest and explicit targets, while absent global `-g` bootstrap uses temporary preview state and does not create `~/.apm`), `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; resolution chain `--target` > apm.yml `targets:` > `apm config set target ...` > auto-detect; this decision is reused by package, MCP, and LSP phases; unresolved required service work fails non-zero before manifest or package writes, and native MCP/LSP write failures also fail non-zero; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` field so a later bare `apm update` reuses them, except absent global dry-runs only preview targets in temporary state; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `grok-build` for stable Grok Build rules, agents, commands, skills, and `AGENTS.md`; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `grok-cloud` after `apm experimental enable grok-cloud` to deploy skills only to `.grok/skills/`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Cursor, OpenCode, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--trust-bin` / `--no-trust-bin` (per-invocation consent for marketplace-plugin bin/ deployment: `--trust-bin` suppresses the trust-posture warning, `--no-trust-bin` skips bin/ even if policy allows; default deploys with a warning), `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skills from a dependency that exposes selectable skills (repeatable; plugin manifests accept a leaf name or source-relative path under skills/; a CLI name or path that matches no declared skill fails before manifest or lockfile commit with available names; a stale persisted `skills:` pin that no longer matches an available source skill warns with the package, declared request names, and available names, and directs the user to edit `skills:` in apm.yml; persisted in apm.yml only on a successful CLI match; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry using that same effective target decision (the shared decision applies, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every explicitly selected target; direct `--mcp` additions are project-scoped and are not valid with `-g`/`--global`), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2). Explicit plugin component paths must resolve inside the plugin root; missing declarations fail before deployment and lockfile commit. | |
| APM only writes to ``opencode.json`` when the ``.opencode/`` directory | ||
| already exists — OpenCode support is opt-in. | ||
| At project scope APM only writes to ``opencode.json`` when the | ||
| ``.opencode/`` directory already exists — OpenCode support is opt-in. |
@microsoft-github-policy-service agree |
0e6fc9f to
5eabce0
Compare
- Changelog update
OpenCode stores MCP servers under `mcp`, but the adapter declared `mcpServers`, so conflict detection never saw servers already in opencode.json. `apm install -g` could then overwrite a same-name server the user configured, and in Cursor too when both targets were selected. - Declare `mcp` as OpenCode's `mcp_servers_key` and add it to the recognized keys in the registry-completeness test. - Render OpenCode's own entry format in `render_server_config`, so legacy ownership adoption still matches the entries APM wrote. - Align the apm-guide `-g --mcp` row with the other rows and the CLI help. - Replace an em dash in the adapter docstring with `--`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`is_semantically_equivalent` compared `mcp_configs` with Python equality, which treats `1` and `true` as equal. A reinstall that only changed OpenCode's `enabled` from `true` to `1` therefore skipped the lockfile write, so the next change back to `true` found no drift, reported the server as already configured and left `opencode.json` at `1`. Compare the configs as JSON instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenCode now supports user-scope MCP, so the global block no longer expects `apm install -g --runtime opencode` to fail. It checks that the install writes `~/.config/opencode/opencode.json`, keeps `enabled: false` and the user's other settings, repeats without changes and leaves the project config alone, and that moving the global target to Codex removes the entry again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cursor now supports user-scope MCP, so `apm install --global --target cursor` writes `~/.cursor/mcp.json` instead of failing as workspace-only. The test still checks that a global install leaves the project's `.cursor/mcp.json` untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5eabce0 to
a30e42a
Compare
Preserve contributor commits and current main behavior while qualifying user-scope Cursor and OpenCode MCP support for microsoft#3104. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep foreign entries unowned across drift and pruning, fail unsafe user configs before no-op and retarget writes, and preserve JSON-exact native adoption. Qualify Cursor and OpenCode through installed lifecycle and canonical boundary regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Persist previous and actually written target ownership before retiring a target. Verify real permission failure, repair, retry and pruning in both global target directions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve current main behavior while qualifying the contributor global MCP change and scoped ownership corrections. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Assert retained names before indexing so mutation evidence reports assertion failures rather than incidental missing-key exceptions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Addresses the Mode B conformance failure by exercising the existing declared-user-capability requirement through real CLI config writes and cleanup. Reuse the contributor lifecycle contract and regenerate its conformance statement; do not waive the gate or amend normative scope. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| test-coverage-expert | 0 | 0 | 0 | Genuine req-tg-014 partition, unmarked-CWD projection and preservation assertions, not a dummy marker. CliRunner fixture tier; e76 installed-E2E reruns and capability-removal controls remain pending. |
| doc-writer | 0 | 0 | 0 | Static review: 1->2 truthfully counts req-tg-014 tests covering capability partition and filesystem lifecycle. Section 8.6 permits additive adapters; no normative extension or installed-binary E2E claim. |
B = blocking-severity findings, R = recommended, N = nits. Counts are signal strength, not gates. The maintainer ships.
The two final specialists reviewed only the conformance recovery. The preceding six-persona runtime delta had no findings; production source did not change afterward. Pending-local-validation caveats in the review snapshots were superseded by the executions below.
Recommendation
Recorded CEO recommendation: The reviewed technical scope warrants shipping; no further product rework is supported. Proceed with publication and actual successor remote CI observation. This advisory recommendation is neither a remote CI success claim nor merge authorization.
The genuine current-head CEO recommendation is ship_now, with no remaining product follow-ups. The publication and successor-CI steps named above are now complete. Preserve normal human review and repository policy.
Lint contract
The exact-head canonical ruff check/format pair, YAML-I/O, 2100-line, portable-relative-path, pylint R0801 and auth-signal gates all passed. Architecture boundaries, 64 quality tests and strict assertion/duplicate ratchets also passed.
CI
- Exact-head unchanged broad selection: 1,893 passed, 1 Windows-only skip, 31 subtests passed; 25 installed-CLI lifecycle scenarios.
- Conformance: 292 passed, 2 existing skips; all 125 requirements align, unwaived Mode B passes, regenerated CONFORMANCE is clean. The new existing-req-tg-014 binding is process-local CliRunner with real filesystem; installed-binary evidence is separate.
- Immutable v2 owner gate: terminal evidence required, 6 owners / 11 executed current-head functional IDs. Semantic verification enforced terminal evidence; no owner exemption or schema change.
| Workflow | Observed result |
|---|---|
| CI | success; 11 actual jobs inspected |
| Spec conformance | success; 1 actual jobs inspected |
| CodeQL | success; 2 actual jobs inspected |
| NOTICE Drift Check | success; 1 actual jobs inspected |
| Deploy Docs | success; 2 actual jobs inspected |
| Merge Gate | success; 1 actual jobs inspected |
One CI recovery repaired the real prior-head Mode B failure; no previous CI run was rebound to this head.
Mergeability status
| PR | Head | CEO | Iterations | Tracked folds | Deferrals | Copilot rounds | CI | Mergeability | Merge state | Notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #3100 | e76a04f | ship_now | 4 | 13 | 0 | 2 | green | MERGEABLE | BLOCKED | normal human review/policy remains |
Convergence
4 outer iterations; 2 Copilot rounds; 1 CI recovery. Final panel recommendation: ship_now. No remaining foldable items or deferrals. Ready for maintainer review. Two normal fast-forward pushes preserve alvedder's original commits and branch.
Folded in this run, contributor fixes, and per-persona findings
Folded in this run
- (panel) Remove the remaining OpenCode project-only claim. (
5e1b6a8b5df7). - (panel) Preserve global scope in the failed-target retry instruction (
5e1b6a8b5df7). - (panel) Pruning can delete a same-name user-owned server. (
5e1b6a8b5df7). - (panel) Existing-server detection bypasses unsafe-path failure. (
5e1b6a8b5df7). - (panel) Legacy OpenCode adoption is not JSON-type exact. (
5e1b6a8b5df7). - (panel) Extend installed-CLI coverage through the global package ownership lifecycle (
5e1b6a8b5df7). - (panel) Exercise direct install -g --mcp for Cursor and OpenCode through the installed entrypoint (
5e1b6a8b5df7). - (panel) Add an installed failed-target preservation contract for unsafe user configurations (
5e1b6a8b5df7). - (panel) Intersect stale names with each runtime's ownership before cleaning global configs (
5e1b6a8b5df7). - (panel) Do not forget previous runtime ownership during direct global retargeting (
5e1b6a8b5df7). - (panel) Persist conservative ownership before retired-target cleanup. (
37f247dedf5a). - (panel) Prove late cleanup failure, repair, retry, and prune in both directions. (
37f247dedf5a). - (panel) Add new-guard removal proof and finish exact-head validation. (
37f247dedf5a). - CI recovery: bind the actual global capability lifecycle to existing
req-tg-014and regenerate CONFORMANCE (e76a04f2f42f), without a normative amendment or waiver.
Copilot signals reviewed
src/apm_cli/adapters/client/opencode.py: LEGIT -- Correct original schema-key ownership defect. Contributor already corrected key and rendering; current installed foreign-preservation coverage extends the regression. Contributor fix5d984f69dbdd; not claimed as worker authorship.docs/src/content/docs/consumer/deploy-a-bundle.md:14: LEGIT -- Valid historical documentation mismatch. Contributor added the promised CHANGELOG entry; the GitHub thread is resolved. Contributor fixd667bb5702b1; not claimed as worker authorship.packages/apm-guide/.apm/skills/apm-usage/commands.md: LEGIT -- Valid original guide contradiction. Contributor aligned the global --mcp row; current worker adds scoped retry and ownership-recovery guidance. Contributor fix5d984f69dbdd; not claimed as worker authorship.src/apm_cli/adapters/client/opencode.py: LEGIT -- Valid repository encoding issue. Contributor replaced the docstring character before this worker; no duplicate worker fix claimed. Contributor fix5d984f69dbdd; not claimed as worker authorship.
Regression-trap evidence (mutation-break gate)
The first 8 guard-removal cycles physically ran at 98de7138, with explicit unchanged-byte composition into the result head. The final 2 capability-removal cycles physically ran at e76a04f2. Each baseline passed, guard removal caused AssertionError, exact bytes were restored, and the restored test passed. No concurrent source readers during mutations.
tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_prune_preserves_skipped_foreign_name[cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_prune_preserves_skipped_foreign_name[opencode],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_package_update_prune_and_uninstall[cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_package_update_prune_and_uninstall[opencode]-- removedsrc/apm_cli/install/mcp/integration.py: scoped_stale = stale_servers.intersection(owned_servers); failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_prune_intersects_each_targets_ownership-- removedsrc/apm_cli/integration/mcp_integrator.py: elif result.get("installed") and installed_servers is not None:; failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_direct_global_mcp_bootstrap_retarget_and_prune[cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_direct_global_mcp_bootstrap_retarget_and_prune[opencode]-- removedsrc/apm_cli/install/mcp/command.py: for previous_target, retired in retired_targets.items():; failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_mcp_refusal_preserves_owned_state[file-symlink-cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_mcp_refusal_preserves_owned_state[file-symlink-opencode],tests.integration.test_architecture_owner_rule_mutations::test_global_mcp_consumer_guards[src/apm_cli/integration/mcp_integrator_install.py-mutation_writes.mcp_target_selection-_raise_strict_config_failures(unsafe_runtimes, console=console, logger=logger)-_raise_strict_config_failures([], console=console, logger=logger)]-- removedsrc/apm_cli/integration/mcp_integrator_install.py: _raise_strict_config_failures(unsafe_runtimes, console=console, logger=logger); failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_legacy_global_opencode_adoption_preserves_json_types[1],tests.unit.install.test_mcp_ownership::test_legacy_adoption_compares_native_opencode_entries[command2-1-expected2],tests.integration.test_architecture_owner_rule_mutations::test_global_mcp_consumer_guards[src/apm_cli/install/mcp/ownership.py-mutation_writes.opencode_enabled_intent-opencode_enabled_matches(existing[name], expected)-True]-- removedsrc/apm_cli/install/mcp/ownership.py: opencode_enabled_matches(existing[name], expected); failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_direct_retarget_failure_retains_old_ownership[cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_direct_retarget_failure_retains_old_ownership[opencode]-- removedsrc/apm_cli/install/mcp/command.py: list(requested_target_servers),; failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_prune_preserves_skipped_foreign_name[cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_installed_global_prune_preserves_skipped_foreign_name[opencode]-- removedsrc/apm_cli/integration/mcp_integrator_install.py: replace_existing=is_update and ( managed_target_servers is None or dep.name in managed_target_servers.get(rt, set()) ),; failed as expected; byte-restored pass.tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_direct_retarget_cleanup_write_failure_can_be_repaired[cursor],tests.integration.test_global_cursor_opencode_mcp_lifecycle::test_direct_retarget_cleanup_write_failure_can_be_repaired[opencode]-- removedsrc/apm_cli/install/mcp/command.py: mcp_target_servers=merged_target_servers, mcp_config_provenance=merged_provenance,; failed as expected; byte-restored pass.tests/spec_conformance/test_manifest_reqs.py::test_declared_user_mcp_targets_install_without_project_signals-- removedsrc/apm_cli/adapters/client/cursor.py: supports_user_scope: bool = True; failed as expected; byte-restored pass.tests/spec_conformance/test_manifest_reqs.py::test_declared_user_mcp_targets_install_without_project_signals-- removedsrc/apm_cli/adapters/client/opencode.py: supports_user_scope: bool = True; failed as expected; byte-restored pass.
Full per-persona findings
test-coverage-expert
No findings.
doc-writer
No findings.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Generated by autopilot-pr-merge-worker. This comment is AI-generated and may contain errors.
Resolve the conflicts with the v0.33.0 release (microsoft#3152) and the Cursor hook and Codex agent fixes (microsoft#3149, microsoft#3150): - CHANGELOG.md: take main's file, which moved the earlier Unreleased entries under 0.33.0, and keep this PR's single entry at the top of the new Unreleased "Fixed" section. - CONFORMANCE.md: take main's file, which adds req-tg-015 to req-tg-017, and keep req-tg-014 at two tests for the global MCP lifecycle binding. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An unchanged `apm install --runtime vscode --target copilot` lost the VS Code deployment record. The legacy migration moved ownership recorded under `vscode` to the `copilot` key the target decision names, and run_mcp_install then pruned that key because the explicit runtime is the only active one. The unconditional re-record of skipped servers hid this until the conflict detector began skipping resolved names and ownership stopped being claimed for skipped entries. Migrate against the explicit runtime, the same selection run_mcp_install makes, so the ownership stays under `vscode`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Scoping stale-server cleanup to recorded per-target ownership stopped `apm install` from removing a dropped server when the lockfile predates ownership tracking and holds only `mcp_servers`. Nothing is recorded to intersect with, so test_stale_mcp_removed_on_update left the entry in `.github/mcp.json`, and the full integration run in the merge queue would have rejected the PR. Restore the earlier cleanup of the targeted runtimes for that case in project scope. User scope stays ownership-only, so user-level entries APM never wrote are kept, and lockfiles that record ownership are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>


Description
apm install -gtreats Cursor and OpenCode as workspace-only MCP runtimes, so global installs skip them (Skipped workspace-only runtimes at user scope: cursor, opencode). Both harnesses do read a user-level MCP config: Cursor loads~/.cursor/mcp.jsonand OpenCode loads~/.config/opencode/opencode.json. APM already deploys other primitives to these user roots, for example OpenCode skills to~/.config/opencode/skills/. Only MCP was refused.This PR makes both adapters user-scope capable:
CursorClientAdapterandOpenCodeClientAdaptersetsupports_user_scope = True, resolve their user-scope config paths, and create the user-level directory when needed. Project scope keeps the existing opt-in: APM still writes only when.cursor/or.opencode/exists.MCPIntegrator.remove_stalefinds the scope-resolved file through staticconfig_path_forhelpers.apm uninstall -gand stale cleanup therefore remove the entries without constructing an adapter or registry client.~/.config/opencode).0600), as in the Gemini and Kiro adapters. Cursor retains native runtime environment references and authored static/source-relative values; this does not restore eager environment substitution.--globalreference, apm-guidecommands.md, user-scope warning text, and CHANGELOG entry.Behaviour changes at project scope, for reviewers:
.cursor/mcp.jsonoropencode.jsonthat APM cannot parse is no longer replaced. The write is skipped and reported as failed.opencode.jsonis now written atomically with0600, matching.cursor/mcp.json.The bounded review follow-through preserves alvedder's commits and original branch. Cleanup intersects each target's recorded ownership; skipped foreign entries are not claimed. Direct retargeting checkpoints prior ownership plus actual successful writes before retiring the old target. If cleanup fails, repairing permissions and retrying retains management of both deployments until cleanup succeeds. Existing native JSON-type and config-safety owners remain authoritative.
Issue and approved scope
Issue: #3104
Human scope-approval comment: #3104 (comment)
This implements the bounded user-scope Cursor/OpenCode MCP lifecycle, including
install -g --mcp, arbitrary working directories, owned-only cleanup, preservation of authored configuration, safe refusal, and owner-only new files. VS Code user MCP, in-place JSONC editing, project opt-in redesign, and the separate issue 3054 are excluded (not additional scope references). The current maintainer dispatch authorizes bounded mergeability work, not merge or scope expansion.Type of change
Testing
Current local evidence: head
e76a04f2f42fff0f8ce24098ca4ff0ddcb891b2d, faithfully composed with main250986fca87a115ccfe9d0b69aa79f784b9e0c90. The all-existing-tests checkbox is not inferred from a bounded selection. Actual remote checks must qualify this head; prior-head CI is not reused.The unchanged bounded selection below passed again on this successor: 1,893 passed, 1 skipped, 31 subtests passed, 298.42 seconds. The skip is the Windows-only IntelliJ path test on macOS.
Ewas the session evidence directory; raw logs and JUnit are retained there.All seven canonical lint gates plus architecture passed on this same head; every command exited 0:
uv run --frozen --extra dev ruff check src/ tests/ scripts/lint_architecture_boundaries.py scripts/architecture_linter/ uv run --frozen --extra dev ruff format --check src/ tests/ scripts/lint_architecture_boundaries.py scripts/architecture_linter/ .venv/bin/python "$E/lint_portable_guards.py" uv run --frozen --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/ scripts/lint_architecture_boundaries.py scripts/architecture_linter/ bash scripts/lint-auth-signals.sh bash scripts/lint-architecture-boundaries.shThe retained portable helper executes the exact CI YAML-I/O, 2100-line, and
str(relative_to)guard predicates on macOS; no exemptions or thresholds changed. Additional current-head results:uv run --no-sync pytest -q tests/quality --junitxml="$E/successor-quality.xml": 64 passed.uv run --no-sync python scripts/check_test_assertions.py: passed, strict ratchet unchanged.uv run --no-sync python scripts/check_exact_test_duplicates.py: passed, 1,268 files, zero allowed duplicate groups.The 25 installed-CLI scenarios use ApmLifecycleRunner subprocesses and isolated HOME/CWDs for both targets: global install, direct
--mcp, reinstall, package update, prune, uninstall and retargeting. They verify native environment/source-relative preservation, foreign servers and top-level keys, legacy JSON-type adoption, HOME aliases, JSONC/unreadable/invalid-map/symlink refusal,0600, and project isolation.Eight serial physical source-guard controls executed at
98de7138f4eee1c3c6aef32b265b3cb2eaf442d8: baseline pass, assertion failure after removal, byte-restored pass. Every corresponding guard and test remains byte-identical at the successor; that composition is explicit, not a relabelled historical execution. Two additional physical controls executed ate76a04f2, independently disabling Cursor and OpenCode user-scope capability: the new conformance assertion fails, then passes after byte-exact restoration. No source readers ran concurrently with these mutations.Earlier validation retained for provenance, not current-head qualification
At
64d15db1, the same broad selection passed 1,893 tests plus 31 subtests, with the same Windows-only skip; original contributor/help/conflict/target-registry scenarios also passed 131 tests. Actual remote CI, CodeQL, NOTICE, docs build and Merge Gate passed, but Spec Mode B failed on 96 substantive critical-path additions without an executable conformance citation. The successor adds the genuine existing-requirement test described below; it does not waive the gate or change runtime behavior.Contributor-reported CPython 3.12 validation before the bounded follow-through: lint and test-quality ratchets clean;
tests/unit tests/test_console.py tests/red_teamreported 23,049 passed;tests/integrationreported 9,353 passed with 17 macOS failures reported to reproduce on main. These historical claims are not substituted for current-result CI. Original tests cover global package/bundle installation from unrelated directories, preservation, refusal, permissions, discovery, and VS Code remaining workspace-only.Spec conformance (OpenAPM v0.1)
If this PR changes behaviour that an OpenAPM v0.1
req-XXXcovers,confirm the three-step ritual in the
development guide:
docs/src/content/docs/specs/openapm-v0.1.mdupdated(new/changed
<a id="req-XXX"></a>anchor + prose + Appendix Crow).
docs/src/content/docs/specs/manifests/openapm-v0.1.requirements.ymlupdated.
@pytest.mark.req("req-XXX")test undertests/spec_conformance/added or extended.CONFORMANCE.{md,json}regenerated viauv run --extra dev python -m tests.spec_conformance.gen_statementand committed.
No normative requirement or manifest amendment: existing
req-tg-014defines capability-driven user-scope MCP selection, and section 8.6 permits additive harness support without a spec revision. The newtest_declared_user_mcp_targets_install_without_project_signalsasserts supported Cursor/OpenCode versus unsupported VS Code, then invokes the existing real-filesystem global CLI install/uninstall contract. This conformance binding is process-local CliRunner/component coverage, distinct from the installed-binary E2E scenarios above. CONFORMANCE records its test count changing from one to two.Exact successor-head conformance commands and results:
uv run --frozen --extra dev python -m tests.spec_conformance.orphan_check BASE_REF=250986fca87a115ccfe9d0b69aa79f784b9e0c90 bash tests/spec_conformance/mode_b_detector.sh uv run --frozen --extra dev pytest tests/spec_conformance -p no:randomly --no-header -q -o junit_logging=all --junitxml="$E/successor-conformance.xml" uv run --frozen --extra dev python -m tests.spec_conformance.gen_statement git diff --exit-code CONFORMANCE.json CONFORMANCE.mdAll passed: 125 requirements aligned; Mode B recognizes the executable existing-requirement citation; 292 tests passed with two preexisting skips; regenerated artifacts have no diff. The existing skips concern publish-timestamp registry coverage and absolute-path schema coverage. No new waiver, threshold change, marker-only test, or requirement exemption was introduced.