Skip to content
Merged
39 changes: 31 additions & 8 deletions .apm/instructions/linting.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,41 @@ report) that claims CI is green.

## CI-mirror commands

The `Lint` job runs:
The `Lint` job runs (see `.github/workflows/ci.yml`):

- `uv run --extra dev ruff check src/ tests/`
- `uv run --extra dev ruff format --check src/ tests/`
1. `uv run --extra dev ruff check src/ tests/`
2. `uv run --extra dev ruff format --check src/ tests/`
3. YAML I/O safety guard (rejects raw `yaml.dump(..., handle)` outside
`utils/yaml_io.py`; mark approved exceptions with `# yaml-io-exempt`).
4. File length guardrail (no `src/**/*.py` may exceed **2450 lines**).
5. No raw `str(path.relative_to(...))` patterns -- use
`portable_relpath()` from `apm_cli.utils.paths`.
6. **Code duplication guardrail (pylint R0801):**
`uv run --extra dev python -m pylint --disable=all --enable=R0801 \
--min-similarity-lines=10 --fail-on=R0801 src/apm_cli/`
7. Auth-protocol boundary check: `bash scripts/lint-auth-signals.sh`

Both must be silent.
All seven must succeed. CI evaluates these on the **PR merge commit**
(HEAD merged with current `main`), so duplication introduced by a
recent main commit can fail your PR even if your own diff is clean.
Always merge `main` locally before running the mirror.

## Local workflow

- **Auto-fix style+imports:** `uv run --extra dev ruff check src/ tests/ --fix`
- **Apply formatter:** `uv run --extra dev ruff format src/ tests/`
- **Verify (must be silent):** `uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/`
- **Verify the full Lint job (must all be silent / exit 0):**
```bash
uv run --extra dev ruff check src/ tests/ \
&& uv run --extra dev ruff format --check src/ tests/ \
&& uv run --extra dev python -m pylint --disable=all --enable=R0801 \
--min-similarity-lines=10 --fail-on=R0801 src/apm_cli/ \
&& bash scripts/lint-auth-signals.sh
```
(The YAML, file-length, and `relative_to` guards are pure-grep one-liners
from `ci.yml`; run them directly if you have touched those surfaces.)

Always run the verify pair before `git push` -- the CI Lint job
Always run the verify chain before `git push` -- the CI Lint job
fails on any remaining diagnostic.

## Common surprises
Expand All @@ -36,11 +57,13 @@ fails on any remaining diagnostic.
- `F401` / `F841` -- remove unused imports / unused locals.
- `SIM103` -- inline negated returns where the body is one line.
- `I001` -- import sort order (auto-fixable).
- `R0801` -- 10+ identical lines across two files. Extract the shared
block into a base class / helper module instead of disabling.

## Lifecycle binding

This is the canonical lint contract for the repo. Skills that
produce artifacts asserting green CI -- notably `pr-description-skill`
(whose "Validation evidence" row covers CI checks) -- inherit this
gate transitively. Do NOT redefine ruff commands inside individual
skills; honor this instruction before invoking them.
gate transitively. Do NOT redefine ruff or pylint commands inside
individual skills; honor this instruction before invoking them.
41 changes: 32 additions & 9 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- Generated by APM CLI from .apm/ primitives -->
<!-- Build ID: a3144f4613b2 -->
<!-- Build ID: 9a566a78e962 -->
<!-- APM Version: 0.14.1 -->

<!-- Source: .apm/instructions/linting.instructions.md -->
Expand All @@ -11,20 +11,41 @@ report) that claims CI is green.

## CI-mirror commands

The `Lint` job runs:
The `Lint` job runs (see `.github/workflows/ci.yml`):

- `uv run --extra dev ruff check src/ tests/`
- `uv run --extra dev ruff format --check src/ tests/`
1. `uv run --extra dev ruff check src/ tests/`
2. `uv run --extra dev ruff format --check src/ tests/`
3. YAML I/O safety guard (rejects raw `yaml.dump(..., handle)` outside
`utils/yaml_io.py`; mark approved exceptions with `# yaml-io-exempt`).
4. File length guardrail (no `src/**/*.py` may exceed **2450 lines**).
5. No raw `str(path.relative_to(...))` patterns -- use
`portable_relpath()` from `apm_cli.utils.paths`.
6. **Code duplication guardrail (pylint R0801):**
`uv run --extra dev python -m pylint --disable=all --enable=R0801 \
--min-similarity-lines=10 --fail-on=R0801 src/apm_cli/`
7. Auth-protocol boundary check: `bash scripts/lint-auth-signals.sh`

Both must be silent.
All seven must succeed. CI evaluates these on the **PR merge commit**
(HEAD merged with current `main`), so duplication introduced by a
recent main commit can fail your PR even if your own diff is clean.
Always merge `main` locally before running the mirror.

## Local workflow

- **Auto-fix style+imports:** `uv run --extra dev ruff check src/ tests/ --fix`
- **Apply formatter:** `uv run --extra dev ruff format src/ tests/`
- **Verify (must be silent):** `uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/`
- **Verify the full Lint job (must all be silent / exit 0):**
```bash
uv run --extra dev ruff check src/ tests/ \
&& uv run --extra dev ruff format --check src/ tests/ \
&& uv run --extra dev python -m pylint --disable=all --enable=R0801 \
--min-similarity-lines=10 --fail-on=R0801 src/apm_cli/ \
&& bash scripts/lint-auth-signals.sh
```
(The YAML, file-length, and `relative_to` guards are pure-grep one-liners
from `ci.yml`; run them directly if you have touched those surfaces.)

Always run the verify pair before `git push` -- the CI Lint job
Always run the verify chain before `git push` -- the CI Lint job
fails on any remaining diagnostic.

## Common surprises
Expand All @@ -37,14 +58,16 @@ fails on any remaining diagnostic.
- `F401` / `F841` -- remove unused imports / unused locals.
- `SIM103` -- inline negated returns where the body is one line.
- `I001` -- import sort order (auto-fixable).
- `R0801` -- 10+ identical lines across two files. Extract the shared
block into a base class / helper module instead of disabling.

## Lifecycle binding

This is the canonical lint contract for the repo. Skills that
produce artifacts asserting green CI -- notably `pr-description-skill`
(whose "Validation evidence" row covers CI checks) -- inherit this
gate transitively. Do NOT redefine ruff commands inside individual
skills; honor this instruction before invoking them.
gate transitively. Do NOT redefine ruff or pylint commands inside
individual skills; honor this instruction before invoking them.
<!-- End source: .apm/instructions/linting.instructions.md -->

---
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Unit test coverage raised to 88% (gate: `fail_under = 80`); integration test coverage raised to 71% with first CI gate at 55%. (#1402)

### Fixed

- Root `.apm` hooks no longer duplicate after renaming the project directory or using git worktrees; Claude, Codex, Cursor, Gemini, and Windsurf hook configs stay idempotent across checkouts. The hook source-id is now derived from `apm.yml`'s `name` field instead of `install_path.name`, and `apm install` silently heals stale same-content entries from prior checkout basenames. Copilot is unaffected (its hooks live in per-file namespaces under `.github/hooks/`, not a shared merged config). (#1392, closes #1329)

## [0.14.1] - 2026-05-20

### Added
Expand Down
34 changes: 34 additions & 0 deletions src/apm_cli/adapters/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,30 @@ def _infer_registry_name(package):

return ""

@classmethod
def _select_best_package(cls, packages):
"""Select the best package for installation from available packages.

Prioritizes packages in order: npm, docker, pypi, homebrew, others.
Uses ``_infer_registry_name`` so selection works even when the
registry API returns empty ``registry_name``.

Args:
packages (list): List of package dictionaries.

Returns:
dict: Best package to use, or None if no suitable package found.
"""
priority_order = ["npm", "docker", "pypi", "homebrew"]

for target in priority_order:
for package in packages:
if cls._infer_registry_name(package) == target:
return package

# If no priority package found, return the first one
return packages[0] if packages else None

@staticmethod
def _warn_input_variables(mapping, server_name, runtime_label):
"""Emit a warning for each ``${input:...}`` reference found in *mapping*.
Expand Down Expand Up @@ -542,6 +566,16 @@ def _replace_runtime(match):

return processed

def _resolve_env_placeholders(self, value, resolved_env):
"""Legacy thin wrapper for backward compatibility.

Kept because external callers and the phase-3 test suite invoke
the pre-#1277 name. Delegates to ``_resolve_variable_placeholders``
with an empty ``runtime_vars`` map. New code should call
``_resolve_variable_placeholders`` directly.
"""
return self._resolve_variable_placeholders(value, resolved_env, {})

# ------------------------------------------------------------------
# Shared server-info helpers (used by all adapter subclasses)
# ------------------------------------------------------------------
Expand Down
23 changes: 0 additions & 23 deletions src/apm_cli/adapters/client/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,26 +548,3 @@ def _select_remote_with_url(remotes):
if url:
return remote
return None

def _select_best_package(self, packages):
"""Select the best package for installation from available packages.

Prioritizes packages in order: npm, docker, pypi, homebrew, others.
Uses ``_infer_registry_name`` so selection works even when the
registry API returns empty ``registry_name``.

Args:
packages (list): List of package dictionaries.

Returns:
dict: Best package to use, or None if no suitable package found.
"""
priority_order = ["npm", "docker", "pypi", "homebrew"]

for target in priority_order:
for package in packages:
if self._infer_registry_name(package) == target:
return package

# If no priority package found, return the first one
return packages[0] if packages else None
23 changes: 0 additions & 23 deletions src/apm_cli/adapters/client/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,29 +1007,6 @@ def _select_remote_with_url(remotes):
return remote
return None

def _select_best_package(self, packages):
"""Select the best package for installation from available packages.

Prioritizes packages in order: npm, docker, pypi, homebrew, others.
Uses ``_infer_registry_name`` so selection works even when the
registry API returns empty ``registry_name``.

Args:
packages (list): List of package dictionaries.

Returns:
dict: Best package to use, or None if no suitable package found.
"""
priority_order = ["npm", "docker", "pypi", "homebrew"]

for target in priority_order:
for package in packages:
if self._infer_registry_name(package) == target:
return package

# If no priority package found, return the first one
return packages[0] if packages else None

def _is_github_server(self, server_name, url):
"""Securely determine if a server is a GitHub MCP server.

Expand Down
Loading
Loading