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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Architecture ownership guards now use a sharded JSON registry and a
single-process Python linter while preserving exact-revision compatibility
and reducing warm median lint time by 75%. (#2739)
- This repository now pins the `copilot` target so plain `apm install` is deterministic across contributor environments. (by @tillig, #2771)

### Fixed

Expand Down
28 changes: 22 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,28 @@ apm install
```

`apm install` reads this repo's [`apm.yml`](apm.yml) (`includes: auto`),
picks up everything under `.apm/`, and deploys it into the harness
directories your coding agent already watches -- `.github/skills/`,
`.github/agents/`, `.claude/skills/`, `.cursor/`, etc. -- depending on
which targets are detected on your machine. Once that is done, your
harness (Claude Code, GitHub Copilot CLI, Cursor, OpenCode, Codex,
Gemini, ...) can discover and invoke the skills by name.
picks up everything under `.apm/`, and deploys it into `.github/` and
`.agents/skills/` -- the tree this repo commits. `apm.yml` pins the
`copilot` target, so every contributor gets that same tree regardless of
which harness their own machine signals.

To deploy to a different harness for one install, first exclude that
harness's generated root locally, then override the pinned target:

```bash
# Claude Code example:
printf '.claude/\n' >> "$(git rev-parse --git-path info/exclude)"
apm install --target claude
```

The local exclusion keeps generated files out of `git status` without hiding
harness configuration from every contributor. The explicit-target install
still adds its deploy paths to `apm.lock.yaml`; leave that change uncommitted.
Your harness (Claude Code, GitHub Copilot CLI, Cursor, OpenCode, Codex,
Gemini, ...) can then discover and invoke the skills by name. For another
target, check its output directory in the
[target catalogue](docs/src/content/docs/concepts/primitives-and-targets.md#target-catalogue);
some targets write to more than one root.

For most PRs, two of those skills carry most of the weight:

Expand Down
7 changes: 7 additions & 0 deletions apm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ includes: auto
author: Microsoft
license: MIT

# Pinned so every contributor's `apm install` deploys the same tree this
# repo commits (.github/ plus .agents/skills/ via skills convergence),
# instead of auto-detecting whichever harness their machine signals.
# Contributors on another harness opt in with `apm install --target <name>`.
targets:
- copilot

# Local-path manifest deps on the extracted skill packages.
# Each was INLINE in .apm/skills/ until the v2 extraction.
# Extraction unlocks consumer reuse of each primitive standalone
Expand Down
22 changes: 22 additions & 0 deletions tests/integration/test_target_resolution_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from click.testing import CliRunner

from apm_cli.cli import cli
from apm_cli.utils.yaml_io import dump_yaml, load_yaml

pytestmark = pytest.mark.integration

Expand Down Expand Up @@ -196,6 +197,27 @@ def test_s05c_apm_yml_both_target_and_targets_error(tmp_path):
_assert_error_three_sections(result.output)


def test_repository_manifest_pin_overrides_second_harness_signal(tmp_path):
"""The repository target pin must override contributor-local harness signals."""
project = _setup(tmp_path, "s02b_copilot_instructions")
repository_manifest = load_yaml(Path(__file__).parents[2] / "apm.yml") or {}
assert repository_manifest.get("targets") == ["copilot"]

fixture_manifest = load_yaml(project / "apm.yml") or {}
fixture_manifest["targets"] = repository_manifest["targets"]
dump_yaml(fixture_manifest, project / "apm.yml")

claude_settings = project / ".claude" / "settings.local.json"
claude_settings.parent.mkdir()
claude_settings.write_text("{}\n", encoding="utf-8")

result = _invoke(["install"], project)

assert result.exit_code == 0, result.output
assert_provenance(result.output, targets=["copilot"], source="apm.yml")
assert list((project / ".claude").rglob("*")) == [claude_settings]


def test_s06_dry_run_no_disk_writes(tmp_path):
"""S6: --dry-run resolves and prints planned writes; no files materialized."""
project = _setup(tmp_path, "s06_dry_run")
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_global_mcp_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def test_user_scope_skips_workspace_runtimes(
runtime=None,
exclude=None,
verbose=False,
apm_config={},
scope=InstallScope.USER,
)

Expand Down Expand Up @@ -167,6 +168,7 @@ def test_project_scope_includes_all_runtimes(
MCPIntegrator.install(
mcp_deps=["test/server"],
runtime=None,
apm_config={},
scope=InstallScope.PROJECT,
)

Expand Down Expand Up @@ -253,6 +255,7 @@ def test_scope_none_treated_as_project(self):
with patch.object(MCPIntegrator, "_detect_runtimes", return_value=set()):
MCPIntegrator.install(
mcp_deps=["test/server"],
apm_config={},
scope=None,
)

Expand Down