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
7 changes: 7 additions & 0 deletions .apm/architecture/owners/install-deployment.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
"version": 1,
"owners": [
{
"id": "orphan-package-selection",
"decision": "Declaration-aware selection of orphaned installed packages",
"owner": "commands/_helpers.py (_find_orphaned_packages)",
"selectors": ["src/apm_cli/commands/_helpers.py"],
"guards": ["install-deployment-orphan-selection"]
},
{
"id": "immutable-dependency-requirements",
"decision": "Commit-based compatibility of immutable dependency edges and frozen lock pins before hoisting",
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,13 @@ jobs:
where git

- name: Run cross-platform contract family
env:
APM_E2E_TESTS: '1'
run: >-
uv run --extra dev pytest -p no:cacheprovider -v
-m windows_compat
tests/unit
tests/integration/test_lifecycle_workspace_lock.py
tests/integration

- name: Diagnostics on failure
if: failure()
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `apm prune` removes unneeded manifestless skill installs after their lock entries disappear, while retaining bundles and whole roots containing needed nested packages. Personal files inside removable package roots are also removed; keep personal source outside `apm_modules/` and preview with `--dry-run`. -- by @fangkangmi (#3057)
- Autopilot maintainer canvas removes a Decide row as soon as GitHub confirms `status/accepted`, without waiting for a full issue/PR refetch.
- Issue and PR triage no longer skip bot-authored items (Copilot, Dependabot, github-actions). They stay in the queue like any other contribution. (#3024)
- PR-review scheduler no longer queues every open pull request. A fresh review requires the `panel-review` label (same trigger as the Agentic Workflow), `status/accepted` on the PR, or an explicit named PR list. The reviewing session also requires `status/accepted` on the PR or a linked issue; otherwise scheduler and review-worker stop with no comment. The worker may clear `panel-review`; the scheduler does not comment or change labels. Both also apply a CODEOWNERS last-comment gate: read the last CODEOWNER comment as conditions and evaluate them against later comments AND labels on the PR and linked issues. Drop or `noop` only when those conditions are unmet or unclear. Named list does not bypass that gate.
Expand Down
12 changes: 9 additions & 3 deletions docs/src/content/docs/consumer/manage-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,21 @@ apm prune --dry-run # preview what gets deleted
apm prune # delete orphaned packages from apm_modules/
```

`apm prune` removes any directory in `apm_modules/` that no longer
corresponds to a declared dependency or a transitive dependency still
required by another package. It does not touch your manifest.
`apm prune` removes unneeded recognized package roots in `apm_modules/`,
including manifestless `SKILL.md` installs whose lock entries are already gone.
It preserves declared direct/dev and retained transitive packages, bundled
skills, and entire roots containing needed nested packages. Personal files
inside a removable root are also removed; keep personal source outside
`apm_modules/`. It does not touch your manifest.
Lockfile entries, deployed harness files (`.github/`, `.claude/`, etc.),
and merged hook configuration owned by the pruned package are all
reconciled immediately by `apm prune` itself -- remaining direct and
transitive packages keep their hooks; no follow-up `apm install` is
required.

See [`apm prune`](../../reference/cli/prune/) for the managed-root boundary
and the separate ownership protections for deployed files.

If you also want to refresh remaining deps to their latest versions or refs, see
[Update and refresh](../update-and-refresh/).

Expand Down
10 changes: 9 additions & 1 deletion docs/src/content/docs/contributing/integration-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,17 @@ environment end-to-end; for local iteration prefer the direct
**On PR and merge queue:**
1. PR-time unit checks and the hermetic Lifecycle Smoke gate run first; merge queue adds Linux smoke, integration, and release-validation gates.

The required Windows compatibility gate selects `windows_compat` tests. Its collection guard requires a non-empty subset, not a fixed test count, so adding marked regressions does not require raising a ceiling. The workflow's test roots and timeout bound scope and runtime.
The required Windows compatibility gate selects `windows_compat` tests within
`tests/unit` and `tests/integration`, with `APM_E2E_TESTS=1` for marked real-CLI
contracts. It runs only that marker subset, not the full integration suite.
Its collection guard requires a non-empty subset, not a fixed test count.
Collection proves a test is selected; a successful Windows job provides
Windows execution evidence. The existing job timeout bounds runtime.

Linux Lifecycle Smoke runs the required marker subset with `-n 2 --dist loadgroup`. Grouped tests stay on one worker, and the six-minute job limit remains unchanged.
Its `lifecycle_smoke and not lifecycle_merge_group` selection is not all
lifecycle coverage: also run affected generated state machines, deployment
ledger, and failure/retry contracts when changing those behaviors.

**On pushed version tag releases:**
1. Unit tests + Smoke tests
Expand Down
30 changes: 23 additions & 7 deletions docs/src/content/docs/reference/cli/prune.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,27 @@ wiring) and rewrites the lockfile.
canonical deployment ownership rows

An installed package is **orphaned** when it is neither declared in either
dependency list nor retained as a lockfile-resolved transitive dependency.
`apm prune` removes the orphan's directory under `apm_modules/`, deletes every
file the orphan deployed into your harness directories (using the
`deployed_files` manifest in the lockfile), removes the entry from
dependency list nor needed by a retained package. This preserves transitive
dependencies, bundled skills, and whole roots containing a needed nested
package. An unrelated sibling root can still be pruned.

Recognized roots under `apm_modules/`, including manifestless `SKILL.md`
packages, are managed installation content. Pruning an eligible root removes
its contents, including manually copied packages and personal files. Keep
personal source outside `apm_modules/`; preview cleanup with `apm prune --dry-run`.
Neither a surviving lock entry nor an `.apm-pin` cache marker is required.

`apm prune` removes the orphan's directory under `apm_modules/`, cleans up its
owned harness deployments using the protections below, removes its entry from
`apm.lock.yaml`, and cleans up empty parent directories.

`apm prune` also parses and reconciles the lockfile's canonical deployment
ownership metadata on every run, even when `apm_modules/` does not exist or
no package is orphaned. It also removes stale direct-dependency records whose
package directory is already absent, allowing a retry to finish after an
earlier lockfile write failed. A stale dependency or owner reference is not
package directory is already absent or retained for a needed nested package,
allowing a retry to finish after an earlier lockfile write failed. Retained
source content does not preserve an undeclared package's deployment ownership.
A stale dependency or owner reference is not
"nothing to prune." If `apm.yml` is missing, the command exits with an error.

## Options
Expand Down Expand Up @@ -125,7 +135,13 @@ reconciles hooks for packages and targets still declared.

Notes:

- Packages that share an install root with a still-declared sibling subdirectory dependency are not falsely protected by ancestor expansion. The check uses lockfile membership (with `apm.yml` fallback) to identify genuine standalone packages.
- Manifestless installs containing `SKILL.md` are detected even after `apm install`
removes their lockfile entry. Skills inside a declared or retained transitive
package remain part of that package and are preserved.
- A recognized ancestor containing a declared direct/dev or retained transitive
dependency is kept intact and reported as retained, not removed.
- Unrecognized directories are not package-removal candidates. Files deployed
outside `apm_modules/` retain the ownership protections described above.
- A manifest embedded at any depth inside an installed package is owned by that
package. It is not an independent dependency, orphan, or prune candidate.
- Deploy paths are validated before deletion; entries that escape the project root are skipped.
Expand Down
9 changes: 6 additions & 3 deletions docs/src/content/docs/reference/lockfile-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,13 @@ the two checks do not double-count.

Orphan detection works in two directions:

- **Orphan packages** - entries in `dependencies` that the manifest no longer
declares. `apm prune` removes them and their `deployed_files`.
- **Orphan packages** - recognized roots under `apm_modules/` no longer needed
by the dependency graph, even when their lock entries are gone. `apm prune`
removes them while preserving bundles and roots containing needed children.
- **Orphan files** - files under managed target directories that no lockfile
entry claims. `apm prune` removes them too.
entry claims. A ghost record alone does not authorize deleting these bytes.
Prune repairs ownership metadata; deletion still requires a pruned
dependency's trusted pre-transition claim and preserves surviving owners.

`apm prune` is the only command that reconciles `deployments` rows. The valid
owner universe and metadata-only repair boundary are defined in
Expand Down
3 changes: 1 addition & 2 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ a collapsed graph; `--force` does not bypass this check.
| `apm uninstall PKGS...` | Remove packages and reconcile their tracked files, MCP servers, and LSP servers; identifier selection is atomic. Accepts `owner/repo`, `name@marketplace`, exact declared local paths, or portable `_local/<name>` keys for direct local declarations with matching lock metadata. A missing or ambiguous identifier exits nonzero before scripts or APM writes. If safe LSP cleanup fails after package removal, the command exits nonzero and preserves the conflicting config; repair it, then run `apm install` (or `apm install --global` for user scope). | `--dry-run`, `-g` global |
| `apm uninstall PKGS...` | Remove packages; identifier selection is atomic. Accepts `owner/repo`, `name@marketplace`, exact declared local paths, or portable `_local/<name>` keys for direct local declarations with matching lock metadata. A missing or ambiguous identifier exits nonzero before scripts or APM writes. A managed hook beneath a symlinked parent is preserved and reported; package removal finishes, but the command exits nonzero because hook cleanup is incomplete. | `--dry-run`, `-g` global |
| `apm uninstall PKGS...` | Remove packages; identifier selection is atomic. Accepts `owner/repo`, `name@marketplace`, exact declared local paths, or portable `_local/<name>` keys for direct local declarations with matching lock metadata. A missing or ambiguous identifier exits nonzero before scripts or APM writes. A managed hook changed after the initial check or beneath a symlinked parent is preserved and reported; package removal finishes, but the command exits nonzero because hook cleanup is incomplete. | `--dry-run`, `-g` global |
| `apm prune` | Remove installed packages absent from the manifest and lockfile-resolved graph; reconcile stale dependency/deployment ownership after interrupted runs without deleting files based only on ghost metadata or dropping shared URI deployments. Orphan deletion failures exit 1 after processing remaining packages and report removed/failed counts. Successful deletions are not rolled back; resolve the errors, then rerun `apm prune`. | `--dry-run` previews package removal and ownership repair without mutation |
| `apm prune` | Remove unneeded recognized roots under `apm_modules/`, including manifestless `SKILL.md` packages after their lock entries disappear. No receipt or `.apm-pin` is required; personal content inside removable roots is also removed. Preserve declared direct/dev and retained transitive packages, bundles, and entire ancestors containing needed children. Outside deployments retain ownership-based protections; stale records never authorize deleting untrusted or shared bytes. Deletion failures exit 1 with removed/failed counts; resolve the errors and rerun to converge. | `--dry-run` previews removal, retention, and ownership repair without mutation |
| `apm uninstall PKGS...` | Remove packages; reconcile tracked files, hooks, MCP, and LSP. Accepts `owner/repo`, `name@marketplace`, exact declared local paths, or portable `_local/<name>` keys with lock metadata. Selection is atomic: missing or ambiguous identifiers exit before scripts or APM writes. Direct and orphan materialized directories are deleted before target cleanup or manifest and lockfile writes. Deletion failure exits 1 without `Uninstall complete` and retains declarations, the on-disk lockfile, and deployed ownership; earlier deletions are not rolled back. Fix permission or file-lock errors, or the unsafe path after a containment refusal, then retry the same command. Restore with `apm install` (`apm install --global` for user scope). A later target cleanup refusal can leave removed directories with retained metadata; resolve the listed files and retry. MCP cleanup attempts every recorded owner. Safe LSP or managed hook cleanup failure preserves the conflicting configuration and exits nonzero after package removal; repair it, then run the scope-appropriate install command. | `--dry-run`, `-g` global |
| `apm prune` | Remove installed packages absent from the manifest and lockfile-resolved graph; reconcile stale dependency/deployment ownership after interrupted runs without deleting files based only on ghost metadata or dropping shared URI deployments | `--dry-run` previews package removal and ownership repair without mutation |
| `apm deps list` | List manifest- and lockfile-resolved packages; ignore parent-owned embedded manifests. Direct locked local packages use actionable `_local/<name>` keys without absolute paths; transitives are removed through their parent. | `-g` global, `--all` both scopes, `--insecure` |
| `apm deps tree` | Show the complete lockfile-resolved tree at any depth; mark repeated ancestors as circular | -- |
| `apm deps why PKG` | Explain why a package is installed (walks lockfile bottom-up to direct deps; analogue of `npm why` / `yarn why`) | `-g` global, `--json` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,22 @@
)
from scripts.architecture_linter.checks.install_uninstall_and_resolution import (
_GUARD_IMMUTABLE_REQUIREMENTS,
_GUARD_ORPHAN_SELECTION,
_GUARD_RESOLUTION_REPLACEMENT,
_GUARD_UNINSTALL_SELECTION,
check_immutable_requirements,
check_orphan_selection,
check_resolution_replacement,
check_uninstall_selection,
)
from scripts.architecture_linter.models import Rule

RULES: tuple[Rule, ...] = (
_rule(
_GUARD_ORPHAN_SELECTION,
"Prune and orphan warnings share declaration-aware package selection.",
check_orphan_selection,
),
_rule(
_GUARD_IMMUTABLE_REQUIREMENTS,
"Immutable dependency compatibility is checked by one owner before hoisting.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,35 @@


_GUARD_UNINSTALL_SELECTION = "install-deployment-uninstall-selection"
_GUARD_ORPHAN_SELECTION = "install-deployment-orphan-selection"
_GUARD_IMMUTABLE_REQUIREMENTS = "install-deployment-immutable-requirements"


def check_orphan_selection(provider: FactsProvider) -> tuple[Violation, ...]:
"""Prune and advisory warnings share declaration-aware orphan selection."""
findings: list[Violation] = []
for path, scope in (
("src/apm_cli/commands/_helpers.py", "_check_orphaned_packages"),
("src/apm_cli/commands/prune.py", "prune"),
):
facts, failures = _facts_for(provider, path, _GUARD_ORPHAN_SELECTION)
findings.extend(failures)
if failures:
continue
if not any(
call.qualname == "_find_orphaned_packages" and call.scope == scope
for call in facts.calls
):
findings.append(
_summary(
_GUARD_ORPHAN_SELECTION,
path,
f"{scope} must route orphan selection through _find_orphaned_packages",
)
)
return tuple(findings)


def check_immutable_requirements(provider: FactsProvider) -> tuple[Violation, ...]:
"""Keep immutable compatibility in one owner and mandatory before hoisting."""
rule_id = _GUARD_IMMUTABLE_REQUIREMENTS
Expand Down
Loading