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.
Objective
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:
- Inspect what has moved upstream since you locked (
apm outdated). - Change on purpose, behind a consent gate (
apm update). - 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:
| 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:
| 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:
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:
| 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:
| 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:
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:
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)
| 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 outdatedreports drift (read-only, exit0even when outdated);apm updatechanges versions — the only version-moving verb, consent-gated;apm auditverifies integrity. -
apm auditis notnpm audit. No CVE database — it checks hidden Unicode and lockfile / drift consistency. Default audit is advisory on drift (exit0);apm audit --cigates the nine consistency checks (exit1). Distinct from Chapter 6'sapm install --frozen, which is a structural presence gate. -
Mind the two over-eager reports.
apm outdatedstays quiet until a branch advances past your lock (pinned deps never light up); theapm update --dry-runplan over-states movement — trust the lockfile diff, not theN updatedcount. -
The lockfile diff is the review artifact. A deliberate
apm updatelands as a PR:apm.ymlshows intent,apm.lock.yamlshows effect — and hereapm.ymlheld while the change lived entirely in the lockfile.apm audit --ciis 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.