Chapter 7

Lifecycle

Keep APM dependencies current while detecting drift and risk deliberately.

Objective

After this chapter you can run APM's maintenance triad — apm outdated, apm update, and apm audit — and keep the three jobs straight: report what has drifted from upstream, change versions on purpose, and verify the result is intact. You will read an apm outdated table, know why a freshly-locked floating ref shows as up-to-date until its branch moves, drive a consent-gated apm update (the only version-moving verb), and read the apm.lock.yaml diff it produces as a reviewable artifact. You will also know precisely what apm audit is — a hidden-Unicode and drift-integrity check, not an npm audit-style vulnerability feed — and when to reach for its --ci gate. This is where Reproducibility stops meaning merely “frozen” and starts meaning “frozen and kept current, deliberately,” with a first look at Provenance / security through the audit check.

Concept/Theory

Reproducible is a floor, not a ceiling

Chapter 6 solved “works on my machine” by freezing the dependency graph: apm install --frozen guarantees a clone matches the lockfile byte for byte. But a frozen graph, left alone, quietly rots. Upstream skills gain fixes, prompts get sharper, instructions track new conventions — and a project pinned six months ago is reproducible and stale. The failure mode from Chapter 1 simply inverts: instead of silent drift you get silent staleness. Reproducibility is a floor, not a ceiling.

So the obvious next worry is: if everything is pinned, how does anything ever move forward — safely? The answer is a deliberate maintenance loop layered on top of the reproducible baseline — a three-beat motion you perform on a cadence, not by accident:

  1. Inspect what has moved upstream since you locked (apm outdated).
  2. Change on purpose, behind a consent gate (apm update).
  3. Verify the result is intact (apm audit).

These are the same motions any team already performs on application dependencies; here they apply to agent context. And they are deliberately not part of the daily habit. The consumer ramp draws the line explicitly: after naming the four everyday commands from Chapter 5, it says “everything else is either lifecycle automation (update, outdated, audit) or a workflow extension” (Use APM packages). The broader lifecycle concept sets the rhythm too — you use “install and run daily, audit in CI, and init and compile rarely” (Lifecycle). This chapter uses lifecycle in that narrower, everyday sense: the three maintenance verbs, not the whole init → install → run → audit flow.

In APM

Three verbs, three jobs

APM splits maintenance into three single-purpose commands so that reporting, changing, and verifying never blur together. Bundling “what's new?”, “change it,” and “is it intact?” into one verb is how package managers historically made updates scary — you never knew what a single command would touch. Three verbs give each step its own, observable blast radius:

The maintenance triad, each verb mapped to its job and what it is allowed to touch. Verified on apm v0.23.1.
Verb Job Writes? Contacts upstream?
apm outdated report drift from upstream No — read-only Yes
apm update change versions — the only mover Yes — rewrites the lockfile (and manifest) Yes
apm audit verify integrity (content + drift) No (unless you pass --strip, which removes hidden characters) No — cache-only replay + local scan

Report: apm outdated shows drift and changes nothing

apm outdated answers “what has drifted from upstream since we locked?” without changing anything. It “reads apm.lock.yaml and queries each remote to detect staleness” and is explicitly “read-only… does not modify apm.lock.yaml or touch apm_modules/” (apm outdated). On Meridian's freshly-restored project it finds nothing to do:

apm outdated on the freshly-installed Meridian project. The pinned tag has no newer match, and the transitive #main skill is locked at the branch's current tip, so both read as current. needs network apm v0.23.1
$ apm outdated
  [*] All dependencies are up-to-date            # exit 0

That “nothing to do” is the chapter's first non-obvious lesson. A freshly-locked floating ref is not outdated, because apm install locked #main to whatever the branch pointed at that day — the lock already equals what the ref resolves to now. Drift is latent in the ref and surfaced by time, not by how loosely the dependency is pinned. A month later, once the upstream branch has advanced past Meridian's locked commit, the same command produces a table:

apm outdated once the upstream branch has moved ahead of the locked commit. The pinned tag stays up-to-date; the moving #main skill is now outdated. The tip SHA is illustrative — #main floats, so your run resolves to a different commit (the content hash stays the same). needs network apm v0.23.1
$ apm outdated
                                                  Dependency Status
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package                                           ┃ Current    ┃ Latest     ┃ Status       ┃ Source               ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ microsoft/apm-sample-package                      │ v1.0.0     │ v1.0.0     │ up-to-date   │ git tags             │
│ github/awesome-copilot/skills/review-and-refactor │ (default)  │ 3169734b   │ outdated     │ git branch           │
└───────────────────────────────────────────────────┴────────────┴────────────┴──────────────┴──────────────────────┘
[!] 1 outdated dependency found                    # exit 0 -- finding drift is not an error

Read the columns and the reason for each status becomes clear:

How apm outdated checks each dependency, and why the pinned tag holds while the branch drifts. Verified on apm v0.23.1.
Column Meaning Pinned tag dep Unpinned branch dep
Current what you have locked v1.0.0 (the tag) (default) (the default branch)
Latest what the ref resolves to now v1.0.0 (same tag) 3169734b (new tip commit)
Status up-to-date / outdated / unknown up-to-date outdated
Source how staleness was checked git tags git branch

A tag-pinned dep is checked against git tags and stays current unless a newer matching tag exists; a branch dep is checked against the branch tip and goes outdated the moment the branch moves ahead of the locked commit. This is exactly the unpinned hazard Chapter 2 flagged, now shown in its lifecycle consequence. Two more facts worth internalizing: apm outdated keys off the committed lockfile, not a live apm.yml edit — loosening a pin in the manifest without re-installing does not change its report — and its exit code is 0 whether or not anything is outdated. Finding drift is not an error; the summary line is informational (exit 1 occurs only when there is no lockfile at all).

Change: apm update is the only version-moving verb

apm update re-resolves every dependency in apm.yml to “the newest version or Git ref allowed by its constraint, prints a structured plan — added, updated, removed, unchanged — and prompts before touching anything” (apm update). It is “the command that actually changes versions in your project” (Update and refresh). Start with --dry-run, the safe preview that “computes and prints the plan … but never writes and never asks”:

apm update --dry-run previews the plan and writes nothing. Note the caveat below: the plan over-states movement. needs network apm v0.23.1
$ apm update --dry-run
  [>] Checking upstream for revision-pin freshness...
  [i] Update plan for apm.yml

    [~] github/awesome-copilot/skills/review-and-refactor
        ref: - (a4aebcd -> -)
        files: .agents/skills/review-and-refactor, .agents/skills/review-and-refactor/SKILL.md, .claude/skills/review-and-refactor, +1 more

    [~] microsoft/apm-sample-package
        ref: v1.0.0 (fb28516 -> -)
        files: .agents/skills/style-checker, .agents/skills/style-checker/SKILL.md, .claude/agents/design-reviewer.md, +13 more

    2 updated
    [~] updated

  [i] Dry run: no changes applied. Re-run without --dry-run to update.   # exit 0 -- lock unchanged

Do not read that plan too literally. It is a re-deploy plan, not a precise “these-commits-will-change” diff: it marks every re-resolvable dep [~] updated — including the pinned v1.0.0 dep that will not move — and it does not pre-compute the destination SHA, rendering it as - (a4aebcd -> -). The 2 updated count over-states real movement. The truth shows only when you run it for real and read the deploy phase (or diff the lockfile). Applied non-interactively as a scheduled job with --yes:

apm update --yes applies the refresh. The deploy phase tells the true story the plan blurred: the pinned dep re-resolved to the same commit; only the moving #main skill advanced. needs network apm v0.23.1
$ apm update --yes
  [>] Checking upstream for revision-pin freshness...
  [i] Update plan for apm.yml
    [~] github/awesome-copilot/skills/review-and-refactor    ref: - (a4aebcd -> -)         files: ..., +1 more
    [~] microsoft/apm-sample-package                         ref: v1.0.0 (fb28516 -> -)    files: ..., +13 more
    2 updated
    [~] updated
  [i] Targets: claude, copilot, cursor  (source: apm.yml)
    [+] microsoft/apm-sample-package #v1.0.0 @fb285168 (cached)     # <- pinned: commit UNCHANGED
    |-- 2 prompts adopted -> .github/prompts/
    |-- 3 agents adopted -> 3 targets
    |-- 4 commands integrated -> .claude/commands/, .cursor/commands/
    |-- 3 rule(s) adopted -> 3 targets
    |-- 1 skill(s) integrated -> .agents/skills/, .claude/skills/
    [+] github.com/github/awesome-copilot/skills/review-and-refactor #default @3169734b   # <- MOVED to tip
    |-- (files unchanged)                                          # <- content identical: bytes didn't change
    [+] <project root> (local)
    |-- 1 prompts adopted -> .github/prompts/
    |-- 2 commands integrated -> .claude/commands/, .cursor/commands/
    |-- 3 rule(s) adopted -> 3 targets
  Updated 2 APM dependencies.                                       # exit 0

There is the whole point of the verb, made concrete: the pinned dependency re-resolved to the same @fb285168 (cached) — a tag pin does not move — while the moving #main skill advanced a4aebcd4 → 3169734b, the branch tip, with (files unchanged) because the file bytes were identical. Only apm update did this; Chapter 5's bare apm install would have restored the old locked commit even for the floating ref. In an interactive session the prompt “defaults to No,” and declining exits cleanly — “no manifest rewrite, no lockfile write, no filesystem changes”; in CI or piped stdin you must pass --yes (apm update). One more distinction to file away: apm update refreshes project dependencies; upgrading the CLI binary itself is a different command, apm self-update (Update and refresh).

Verify: apm audit checks integrity, not vulnerabilities

apm audit confirms deployed context is intact with two independent checks: a content scan for hidden Unicode in deployed prompt / instruction / skill / rules files, and an install-replay drift check that re-materializes from cache into a scratch tree and diffs it against your working tree. This protection “already runs automatically in apm install” — apm audit is the on-demand re-verification, not a gate you must remember to call to be safe (apm audit). Because it replays from cache and scans local files, it needs no network:

Default apm audit on the refreshed project: content scan plus install-replay drift, both clean. Runs offline. apm v0.23.1
$ apm audit
  [>] Scanning all installed packages...
  [>] Replaying install (cache-only)...
  [+] Replayed 3 package(s)
  [>] Diffing scratch vs working tree...
  [+] No drift detected

  [*] 26 file(s) scanned -- no issues found          # exit 0

For CI you want a machine-readable gate, and that is apm audit --ci: it adds a nine-check lockfile-consistency table on top of the drift replay. This is the Provenance / security guardrail that makes a refresh trustworthy:

apm audit --ci — the lockfile-consistency gate. Nine checks, all green. Runs offline. apm v0.23.1
$ apm audit --ci
  [!] Could not determine org from git remote; enforcement skipped (set policy.fetch_failure_default=block in apm.yml to fail closed)
  [>] Replaying install (cache-only)...
  [+] Replayed 3 package(s)
  [>] Diffing scratch vs working tree...
  [+] No drift detected

                                            [>] APM Policy Compliance
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Status   ┃ Check                    ┃ Message                                                                  ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ [+]      │ lockfile-exists          │ Lockfile present                                                         │
│ [+]      │ ref-consistency          │ All dependency refs match lockfile                                       │
│ [+]      │ deployed-files-present   │ All deployed files present on disk                                       │
│ [+]      │ no-orphaned-packages     │ No orphaned packages in lockfile                                         │
│ [+]      │ skill-subset-consistency │ Skill subset selections match lockfile                                   │
│ [+]      │ config-consistency       │ No MCP configs to check                                                  │
│ [+]      │ content-integrity        │ No critical hidden Unicode or hash drift detected                        │
│ [+]      │ includes-consent         │ 'includes:' declared -- local content deployment is explicitly consented │
│ [+]      │ drift                    │ no drift detected against lockfile                                       │
└──────────┴──────────────────────────┴──────────────────────────────────────────────────────────────────────────┘

  [*] All 9 check(s) passed          # exit 0

The choice of form matters because the two modes carry different exit codes on the same problem — and that is the difference between an advisory report and a build gate:

The three apm audit exit codes, by mode. Default audit is advisory on drift; only --ci gates it. Verified on apm v0.23.1.
Situation apm audit (default) apm audit --ci
Clean 0 0
Drift only (missing / modified deployed file) 0 — advisory 1 — gate fails
Hidden-Unicode finding 2 — content scan 1 — via content-integrity

The benign Could not determine org from git remote line appears only because the sample project has no git remote, so organization-policy discovery is skipped. In a real repo with a remote, org policy would be fetched and enforced — that fleet story is Chapter 11, not this chapter.

When to use / pitfalls

The division of labor

Six commands touch a locked project, and keeping them straight is most of the skill. Only one moves a version; only one fails a build:

Every command that touches a locked project, by role. Only apm update moves a version; only apm audit --ci fails a build. Verified on apm v0.23.1.
Command Role Moves versions? Fails on drift?
apm install (bare) restore locked bytes (Ch 5) No No
apm install --frozen enforce structural reproduce (Ch 6) No Presence gate
apm outdated report drift (read-only) No No (exit 0)
apm update change — the only mover Yes n/a
apm audit verify content + drift No Advisory (exit 0)
apm audit --ci gate lockfile consistency No Yes (exit 1)

The one-line mental model: install restores · --frozen enforces · outdated reports · update changes · audit verifies · audit --ci gates. Note the two integrity checks are not the same tool wearing two hats. apm install --frozen from Chapter 6 is a structural guardrail — it refuses to resolve anything new and fails if the manifest and lockfile have drifted apart before deploying. apm audit --ci is an integrity guardrail — it replays the deployed tree and runs the nine consistency checks (content hashes, orphaned packages, hidden Unicode, on-disk drift). Use --frozen at install time to prove the lock is honored; use audit --ci to prove the deployed bytes are intact. Both belong in CI; neither is a CVE feed.

Worked example

Meridian's first monthly refresh, as a pull request

The staff engineer's scheduled job runs the triad in order — report, change, verify:

The monthly refresh, three verbs in order. outdated and update contact upstream; audit --ci runs offline. needs network apm v0.23.1
apm outdated            # report: the #main skill drifted past our lock
apm update --yes        # change: move it on purpose, re-lock (the only version-moving verb)
apm audit --ci          # verify: 9 integrity checks pass -> attach as PR evidence

The whole reviewable surface of that change is the apm.lock.yaml diff. It is plain YAML you can read “to answer ‘what version am I actually running?’ without trusting the manifest, which may have floating refs” (Manage dependencies). Here it is — and what it does not change is as instructive as what it does:

The apm.lock.yaml diff a deliberate apm update produced — one resolved commit plus the timestamp. The reviewable artifact. apm v0.23.1
--- apm.lock.yaml (before update)
+++ apm.lock.yaml (after update)
-generated_at: '2026-07-02T01:17:41.266376+00:00'
+generated_at: '2026-07-02T01:23:19.474392+00:00'   # advanced: update rewrote the resolved content

   - repo_url: github/awesome-copilot                 # the transitive #main dep
-  resolved_commit: a4aebcd4bd354b59a6a6c12ab9c5c98a9d9e0276   # old (behind)
+  resolved_commit: 3169734bc2fb25d5e092130fc93d24b0dee3ac3a   # new (branch tip)
What the refresh changed in the lockfile — and what it deliberately left alone. Verified on apm v0.23.1.
Field Before After Why
generated_at …01:17:41… …01:23:19… advanced — update writes; a pure restore leaves it byte-stable (Ch 6)
github/awesome-copilot resolved_commit a4aebcd4… 3169734b… the only real move — floating #main re-resolved to the tip
github/awesome-copilot content_hash sha256:9236d06a… sha256:9236d06a… unchanged — the commit moved but the bytes did not (content-addressing, Ch 6)
microsoft/apm-sample-package resolved_commit fb285168… fb285168… unchanged — a tag pin does not move under apm update

This is the headline of the chapter. An agent-context update is as reviewable as any dependency bump. The pull request's review surface is the two files the book has built toward since Chapter 4: the apm.yml diff shows intent and the apm.lock.yaml diff shows effect. In this refresh the apm.yml is unchanged — and that emptiness is itself the evidence that the pins held: the only thing that moved was a transitive floating ref, recorded entirely in the lockfile. A reviewer opens both, sees one resolved_commit advance and everything else hold, and reads the attached apm audit --ci pass as proof the result is intact. APM frames this mental model directly: for SHA-pinned deps, “think of apm update as Dependabot-style review for AI packages” (Manage dependencies). The change is exactly as visible as the team's PR discipline makes it — deliberate change, not silent drift.

Recap & next

Recap

  • Reproducible is a floor, not a ceiling. A frozen setup still needs a deliberate maintenance loop — inspect → change → verify — to stay current without giving up Reproducibility. Staleness is just drift wearing a different mask.
  • Three verbs, three jobs. apm outdated reports drift (read-only, exit 0 even when outdated); apm update changes versions — the only version-moving verb, consent-gated; apm audit verifies integrity.
  • apm audit is not npm audit. No CVE database — it checks hidden Unicode and lockfile / drift consistency. Default audit is advisory on drift (exit 0); apm audit --ci gates the nine consistency checks (exit 1). Distinct from Chapter 6's apm install --frozen, which is a structural presence gate.
  • Mind the two over-eager reports. apm outdated stays quiet until a branch advances past your lock (pinned deps never light up); the apm update --dry-run plan over-states movement — trust the lockfile diff, not the N updated count.
  • The lockfile diff is the review artifact. A deliberate apm update lands as a PR: apm.yml shows intent, apm.lock.yaml shows effect — and here apm.yml held while the change lived entirely in the lockfile. apm audit --ci is the evidence. Give the loop an owner and a cadence — Meridian chose monthly.

Next

apm audit was your first look at the checks that keep agent context trustworthy — and those same hidden-Unicode and content-integrity scans run automatically on every install, before anything reaches disk. Chapter 8 — Security by Default opens that install-time pipeline: content-hash pinning, hidden-Unicode blocking, and why transitive MCP servers are held back — the Provenance / security property made mechanical, not optional.