After this chapter you can read the source identities, hashes, and deployment ownership in
apm.lock.yaml, compare a restored setup with its recorded state, distinguish
apm install --frozen's limited structural checks from content audit, and use
apm lock and apm find to separate resolution from deployment and trace
a tracked file's origin.
Edition 1.2 — inspected CLI: APM 0.31.0.Chapter 5 made the lockfile appear as a by-product; here it
becomes the subject — Reproducibility
made mechanical. Current examples use small recorded fixtures; the explicitly historical
v0.23.1 snapshot keeps its original evidence boundary.
The package installs in Chapter 5 wrote
apm.lock.yaml. You noticed resolved_commit and content_hash
in the Git example without yet studying them. Now ask the lockfile question familiar from npm,
pip, or Cargo: how does another checkout reproduce a reviewed result rather than resolve
whatever is newest?
The answer is a distinction. A manifest declares intent, and intent is often a moving
target: a dependency's ref can be a branch (main), a tag (v1.2.0), a commit
SHA, or a semver range (^1.2.0). A branch can move; a range can admit a newer matching
tag. Two fresh resolutions without the same lock can therefore select different
content from the same manifest. That is the agent-context version of “works on my machine.”
The lockfile records the resolution and deployment ownership. For unchanged Git
declarations, install reuses the recorded graph, including transitive commits and manifests.
Upstream changes do not silently become new inputs to that replay. Ordinary install can instead
reconcile edited declarations or local sources; a missing lock permits fresh resolution.
Keep the manifest, lock, local sources, targets, and CLI version together when comparing results
(0.31.0 lockfile and replay contract).
Three questions, source-specific answers
Separate what you requested, what was selected, and how content is checked.
For Git, resolved_ref → resolved_commit →
content_hash illustrates those questions. It is not a universal field set:
Source identity in real 0.31.0 fixtures, not a synthetic combined lock schema.
This emitted entry has deployed hashes, no Git ref/commit or package-tree hash.
Its source directory must travel with the project; a path is not a version pin.
source: registry, exact version/ref 1.0.0,
archive resolved_url and resolved_hash; no Git commit.
resolved_hash fingerprints the archive; package-tree and deployed hashes
describe different content. This fixture used an anonymous loopback server, not a public hub.
A source coordinate answers where; a hash answers which content.
Neither authenticates a publisher or proves a prompt benign.
name and ordinary Git/local version are self-asserted inventory metadata.
The pinned skill's version: unknown does not weaken its immutable Git commit; registry
and Git-semver versions have separate resolution roles
(identity and metadata fields).
The hash follows the bytes, not the ref
The historical v0.23.1 Meridian captures make this distinction concrete:
separate resolutions selected transitive commits a4aebcd4… and
3169734b…, yet the selected skill's tree hash stayed
sha256:9236d06a…. These were not two replays of one unchanged lock.
The manifest's transitive edge floated; each generated lock still pinned its selected commit.
The risk is the next fresh resolution, not an upstream branch overriding an existing pin.
Package-tree content_hash: sorted relative paths and actual
materialized file bytes. GitCache-backed subpath checkouts set core.autocrlf=false,
but that does not override every .gitattributes or core.eol rule, or
rewrite CRLF committed in source.
Deployed-file hashes: UTF-8 text normalizes CRLF to LF; binary content is hashed
raw. Changing only line endings can therefore change disk bytes without changing the canonical
deployed-text hash. Copilot, Claude, and Cursor projections can legitimately have different
frontmatter and different hashes.
The 0.31.0 instruction probe changed all six deployed files to CRLF: CI audit exited
0, with the lock unchanged. That proves the deployed-text normalization boundary,
not universal cross-platform package-tree or cross-harness byte equality
(hash definitions).
Generated, never hand-edited
apm.lock.yaml is a generated record, not an authored file. You
edit apm.yml; APM writes the lockfile. The review story lives in the diff:
regenerate after an intended change, then review which identities, hashes, and ownership rows
moved. Generated does not mean authenticated — protect and review both files.
Reproducibility is not staleness, either: refreshing an unchanged declaration deliberately is
the apm update workflow in Chapter 7.
Read the file as a resolved inventory plus an ownership record, not just a list of versions.
The worked fixture contains one local package and one workspace
instruction, each projected to Meridian's three targets. Its real lock makes the
local-versus-Git distinction visible:
Selected fields and two intact ownership rows from
backend/examples/updates/1.2/operations/repro/apm.lock.yaml.
Other rows and compatible file lists/hash maps are omitted. Use the complete fixture lock
for replay, never this excerpt.
apm v0.31.0 — observed
Each deployments row identifies a destination through kind,
target, value, runtime, and scope.
owners records its claimants, oldest to newest; active_owner names the
current one and must be a member of that list.
Here the dependency's ownership key is ./package, while . owns the
workspace instruction. A row's content_hash describes that deployed content,
not the dependency's entire tree. Native-entry and directory rows need not carry a file hash
(canonical ownership rows).
The compatible per-dependency deployed_files list and deployed_file_hashes map
remain in the full file. Root .apm/ outputs also retain
local_deployed_files/local_deployed_file_hashes.
A larger Git graph adds depth and resolved_by for transitive provenance.
An MCP-only project can have an empty APM dependency list alongside mcp_servers,
mcp_configs, mcp_target_servers, and URI ownership rows.
Do not infer one dependency shape from another.
New locks omit generated_at. A legacy timestamp was retained on an
unchanged restore, but it is not a change oracle. Git-semver locks still record
constraint, resolved_tag, and resolved_at under format
'2'; registry locks also use format '2'. Even apm_version
is optional: Chapter 5's root-local-only lock omitted it. Prove a no-op by comparing
complete lock bytes and expected outputs, as in the
current restore exercise, not by watching a timestamp or
an installed-package counter.
apm install --frozen — a limited structural gate
CI should not silently invent an unreviewed dependency resolution. Frozen install requires an
existing lock and checks structural conditions, including missing direct Git dependencies and
declared MCP configuration. But it is not a universal declaration-change veto.
The independent Chapter 5 check
added a new local path declaration while retaining the old lock: frozen install exited
0, deployed the new skill, and rewrote that lock. The direct-dependency presence
checker skips local paths
(0.31.0 structural check).
Bounded 0.31.0 observations for apm install --frozen, not a universal consistency test.
Situation
Exit
Observed boundary
Lockfile missing entirely
1
Requires a lock; no project writes.
New direct Git package absent from the old lock
1
Names the missing package; no project writes. Priya's case.
New local path declaration, old lock retained
0
Installed the new local skill and changed the lock.
Declared local source directory missing
1
A local source must actually exist; it cannot be fetched from a Git pin.
Full-SHA selector changed to the public tag at the same commit
0
Lock metadata changed; not a literal-ref-string gate.
Already locked transitive skill declared directly at its locked SHA
0
Lock changed to represent the direct declaration.
Root MCP arguments changed in the manifest, old lock retained
1
Manifest-to-lock MCP configuration differs.
Native output edited, declaration unchanged
0
The tested instruction was restored; the tested native MCP edit was left unchanged.
Neither was rejected as content drift by the frozen preflight.
Frozen install can materialize files and change lock metadata; its successful exit does not
prove that the committed contract stayed unchanged. Nor is it an audit of native file bytes.
The CLI's reminder is literal:
Lockfile presence verified. Run 'apm audit' for on-disk content integrity.
With unchanged Git declarations, a moving upstream branch is handled by replaying the locked
commit, not by comparing the remote branch during preflight
(frozen mode and locked replay).
Audit can replay in scratch — cold is not offline
apm audit --ci checks supported content, consistency, and ownership separately.
It can acquire locked sources into an owned scratch replay when modules/cache
are absent. Clean public and local cold probes left the checkout unchanged; public replay with
acquisition deliberately unavailable exited 1, not a harmless skip.
Cold public restore or CI audit may need network access.
A warm cache or local-only success is not proof of an offline deployment plan
(CI scratch replay).
Audit is not a universal native-configuration check either: in the tested MCP-only project,
CI audit did not detect missing or edited native MCP configuration. Frozen install recreated
the missing file but left the edit intact. Check such harness-specific state separately rather
than treating a clean exit as byte certification. Chapter 7 develops
audit; Chapter 8 develops its security boundaries.
apm lock — write the contract without deploying
Sometimes you want to resolve a declaration for review without altering harness files.
apm lock separates those jobs: it can acquire/materialize dependency metadata and
write the lock, but does not deploy or delete harness files. Crucially, it
preserves existing deployment state for later reconciliation
(lock-only behavior).
Run this command once in each separate local scratch copy described below.
Both recorded runs exited 0; neither deployed harness files. Offline fixture.
apm v0.31.0 — observed
apm lock
Same lock-only command, different prior deployment state.
Scratch setup
Recorded result
Installed repro; keep its lock, delete only
.github/instructions/library.instructions.md as a canary.
Source-only repro; no previous lock or harness outputs.
Resolution-only lock and modules appeared; no deployment rows or harness files.
Keeping ownership is not proof that files are still present. After a target contraction,
lock-only generation also retained Claude/Cursor files and their ownership; ordinary install
performed the cleanup. Use apm lock to compute the record without deployment,
not to repair a missing file or erase cleanup history.
An SBOM timestamp is a different clock
Inventory export serializes a lock; it does not re-resolve or authenticate its contents.
The inspected help documents apm lock export --timestamp as an
SBOM timestamp control, with fallback to SOURCE_DATE_EPOCH, a legacy
lock timestamp, then the Unix epoch. That help/source contract is distinct from execution evidence:
separate producer probes actually exported CycloneDX and SPDX and repeated the fixed-timestamp
CycloneDX export byte-identically. Neither operation restores generated_at to new locks
(inventory export).
Recorded example using the genuine lock in
backend/examples/updates/1.2/producer/pinned-bundle, not a run on repro.
Export reads the existing lock without fetching packages; both identical
invocations exited 0. apm v0.31.0 — observed
“Where did this instruction come from?” is the provenance question behind
apm find. It reads the lock's deployed-file records; --source adds the
source coordinate. This is a lookup, not a fresh hash check or publisher authentication
(provenance lookup).
Lookup in the installed repro fixture: dependency, workspace source, then a deliberate
untracked-path query. Recorded exits: 0 / 0 / 1. Offline.
apm v0.31.0 — observed
The first reports _local/package ./package; the second reports
. (workspace). The third reports that the path is not tracked. Compare those answers
with the two ownership rows: source identity and deployment
ownership answer related but different questions.
Historical Meridian provenance snapshot — APM 0.23.1
Original full-package lookup capture, 2026-07-02. --path shows the transitive chain;
it remains in the 0.31.0 help, but this transcript was not rerun at that version.
The commit prefix is historical: an unpinned manifest edge still had a pinned commit in this
lock. Lookup is offline and says nothing about current on-disk integrity.
apm v0.23.1
$ apm find .github/instructions/design-standards.instructions.md
microsoft/apm-sample-package # the owning package
$ apm find .github/instructions/design-standards.instructions.md --source
microsoft/apm-sample-package microsoft/apm-sample-package@v1.0.0 # pinned -> @tag
$ apm find .agents/skills/review-and-refactor/SKILL.md --path
github/awesome-copilot
apm.yml -> microsoft/apm-sample-package -> github/awesome-copilot # the transitive chain
$ apm find .agents/skills/review-and-refactor/SKILL.md --source
github/awesome-copilot github/awesome-copilot@3169734bc2fb # unpinned -> @commit
$ apm find .github/instructions/meridian-checkout.instructions.md --source
. (workspace) # your own .apm/ source, not a package
$ apm find .github/does-not-exist.md
[x] '.github/does-not-exist.md' is not tracked by any installed package in apm.lock.yaml. # exit 1
Choose the job first: replay, reconcile, verify, resolve without deployment, or trace provenance.
A successful command answers its own question, not all five.
Which lockfile-related command to reach for, by intent.
You want to…
Run…
Because
set up or re-materialize a cloned repo
apm install (bare)
Replays unchanged Git intent; reconciles changed declarations or local sources
(Chapter 5).
materialize a reviewed contract in CI
apm install --frozen
Requires a lock and performs limited structural checks; also reject unexpected
manifest/lock changes.
check supported deployed content and ownership
apm audit --ci
Separate integrity/replay checks; cold acquisition can need network and native coverage varies.
Meridian shipped v0.2.0 in Chapter 5 with one
pinned dependency and one review script. Priya's next lesson is to commit the
regenerated lock, not merely the new declaration. Rehearse that boundary with a small
separate fixture, rather than an inaccessible private package or an invented
lockfile diff. This exercise does not change Meridian's milestone or canonical manifest.
1. Start with real local inputs
Use APM 0.31.0,
not a moving latest release. From
backend/examples/updates/1.2/operations/repro/, copy the two manifests below plus
.apm/instructions/workspace.instructions.md and
package/.apm/instructions/library.instructions.md into a new short scratch directory
outside the book checkout. Include the hidden directories. These sources contain only small
instructions: no executable primitives or network dependencies.
For this first-generation exercise, keep the fixture's genuine apm.lock.yaml
separately for comparison; start without modules, native outputs, or a lock in scratch.
A real clone-and-restore workflow instead brings the committed lock from the start.
Complete repro/apm.yml: a local dependency and three explicit destinations.
Offline fixture. apm v0.31.0 — observed
# Recorded identity, three target projections, and a local dependency.
name: operations-repro
version: 1.0.0
targets: [copilot, claude, cursor]
dependencies:
apm:
- path: ./package
mcp: []
includes: auto
Complete repro/package/apm.yml, accompanying its library instruction.
Local identity is a path to these sources, not a Git SHA.
apm v0.31.0 — observed
# A safe local source; no network or executable primitives.
name: operations-library
version: 1.0.0
dependencies: {}
2. Generate, replay, then check
Run these consecutively in the source-only scratch copy. The first install creates the recorded
state; the next two replay unchanged inputs. The last command asks the separate integrity
question. --no-fail-fast -f json requests all diagnostic checks in JSON, not a
different integrity policy.
Generation, bare replay, frozen replay, and CI audit of repro.
Recorded exits: 0 / 0 / 0 / 0. Offline.
apm v0.31.0 — observed
The complete lock's recorded SHA-256 after each command was
79fe2a70ae0c8b9987ddd0105d013c6593a6c3fd8135ae94601d4df58fd22772.
This is a hash of the entire lockfile, not a dependency field.
generated_at was absent. Compare your complete lock and the native files below;
do not substitute a timestamp, summary message, or exit code for those observations.
Six native outputs of the local fixture; the authored inputs remain in place.
This clean instruction fixture exercises content and ownership, not live harness behavior or
organization-policy enforcement. For acquisition from an empty cache, use
Chapter 5's pinned public skill:
its cold frozen restore preserved the lock while fetching the recorded source.
That variant requires public Git access on a cold cache.
3. Reproduce the forgotten Git lock
Make a separate copy of the successfully installed fixture. Keep its old lock and all its sources,
but replace only the manifest's dependencies block with this one. The extra package
is real and pinned; it is deliberately absent from this copy's lock:
Edited dependency block only — retain the rest of repro/apm.yml and its old
lock. This negative test stops before fetching the public package.
apm v0.31.0 — observed
# Scratch-only edit: new Git declaration, old local-only lock.
dependencies:
apm:
- path: ./package
- microsoft/apm-sample-package#v1.0.0
mcp: []
Frozen install in the mismatched scratch copy: expected exit 1, no project writes
or package fetch. apm v0.31.0 — observed
apm install --frozen
The diagnostic names microsoft/apm-sample-package as declared in
apm.yml but missing from apm.lock.yaml. This is Priya's mistake
mechanically, not an upstream ref moving. Do not replace the Git addition with a new local path
and expect the same refusal: the independent local-path result
was exit 0 with a changed lock.
Discard the disposable mismatch copy when the exercise is complete. In a real pull request where
the addition is intended, Priya reconciles with ordinary apm install, reviews the
genuinely generated lock and deployments, and commits them alongside the manifest.
Acquiring that public Git graph can require network access.
The rule is regenerate, review, commit — not hand-author a plausible SHA or accept whatever
a CI install happened to write.
Repeat the bounded checks automatically
The existing runner rehearses frozen replay, per-target drift, invalid ownership, CRLF hashes,
and the retained-lock canary in owned scratch copies. It needs Python 3.10+ and PyYAML 6;
use the absolute prepared APM executable. Expected negative audit exits are assertions of
detection, not a claim that every package type is covered.
Reusable local checks; substitute your prepared executable's absolute path.
The runner preserves results and test projects outside the checkout.
apm v0.31.0 — explorer fixture suite
$Apm = '<absolute path to the prepared APM 0.31.0 executable>'
python .\backend\examples\updates\1.2\operations\verify.py --apm $Apm --suite repro
The manifest names intent; the lockfile freezes the fact.apm.lock.yaml
records resolved source identities and deployment ownership. Unchanged Git intent replays the
locked graph; changed declarations or local sources require reconciliation.
Identity depends on source type. Git commits, local paths, and registry
archive identities are not interchangeable. Package-tree hashes use actual bytes;
deployed UTF-8 hashes normalize CRLF.
Frozen checks are limited. Missing locks and the tested missing Git declaration
fail; a newly declared local path can install and rewrite the old lock. Compare the committed
contract before/after, and audit supported content separately. Cold CI replay may need network.
apm lock leaves harness files alone and retains existing deployment state.
A fresh resolution-only lock has no previous ownership to retain.
apm find traces recorded files; neither a lookup nor an SBOM authenticates a publisher.
Never hand-edit the lockfile. Fix the manifest, regenerate, review the diff like a
dependency change, and commit the re-locked file. New locks omit generated_at;
the current exercise proves a no-op with complete lock bytes and expected native outputs.
Meridian's CI break was a new Git declaration missing from the committed lock,
not an upstream ref moving. Priya's lasting fix is reconcile-review-commit.
Next
Reproducibility holds the graph still; the next question is how to move it deliberately.
Chapter 7 — Lifecycle opens the intentional-change door named here:
apm outdated to report available versions, apm update to refresh on purpose,
and apm audit — the separate integrity companion to --frozen — to
check the resulting supported content and ownership.