Editing a built-in skill costs two things

The built-in agents and skills are the default chat instructions of every deployment. They live in src/MeshWeaver.AI/Data/{Agent,Skill}/, ship embedded in MeshWeaver.AI.dll, and are served on every portal by BuiltInAgentProvider / BuiltInSkillProvider. Editing one is deliberately expensive: two separate mechanisms record the change, and only one of them is loud.

The two consequences

What moves How you find out
1. The module locks 22 manifest.lock files — every module that bundles src/MeshWeaver.AI carries the file's content hash, so AI, Acp, Anthropic, Chat, Edu, Mcp, Teams, OpenAI, … all move scripts/validate-repos.py says manifest.lock is stale (expected moduleVersion …) and names the fix. Loud, immediate, and it looks complete.
2. The content-pack ledger ONE normalised content hash in src/MeshWeaver.AI.Test/TestData/AiContentPackSync.json AiContentPackDriftTest.EveryFile_StillHashesToItsPinnedReconciliationPoint, in src/MeshWeaver.AI.Testa project whose sources you did not touch

Why the second one gets missed

Because the first one answers the question you were asking. Measured on Plugins#2147 (2026-09-19): editing Data/Skill/recycle-after-deploy.md moved 22 locks. Regenerating them, re-running validate-repos.py and reading ✓ 315 node(s) valid across 72 repo(s) is a complete, green, self-consistent answer to "what else does this change touch?" — and it is wrong, because the ledger is not a manifest and validate-repos.py does not read it.

Nothing else points at it either. The edited file is markdown; no NodeType recompiles; no project under src/ references it in a using; and the suite that pins it is selected by the module closure, so on a PR it runs after the locks are already green.

This is the fleet's recurring shape, not a local surprise: the VISIBLE consequence is not the denominator. The same reasoning error lands as a sweep's count: 0 over partitions it could not read, and as a green gate that checked a literal roster instead of the folder.

What to actually do

Run src/MeshWeaver.AI.Test whenever you touch src/MeshWeaver.AI/Data/** — not because your diff compiles into it, but because it is where the decision is recorded.

dotnet test src/MeshWeaver.AI.Test/MeshWeaver.AI.Test.csproj -c Release --no-build \
  -p:MeshWeaverRoot=<platform checkout> --filter-class "*AiContentPackDriftTest"

The guard hands you the replacement hash in its own failure message, which is why this costs two minutes once you know to look:

Drifted: Skill/recycle-after-deploy.md: pinned fd781d68214e… →
         re-pin "core" to "0830a07f60c37f557efa43101a0713fc229fc075d62f4da45fbfad7fdf9365d7"
total: 15, failed: 1, succeeded: 14

Paste that hash into the entry's core field and say in its note what changed. The note is the point of the ledger, not paperwork around it: the rule the test enforces is that an edit to a built-in agent or skill is a recorded decision rather than an accident, and a re-pin with no note satisfies the hash while defeating the reason the hash exists.

Re-run and require the positive signal — total: 15, failed: 0, succeeded: 15. An exit code alone is not evidence here: the suite exits 0 for a project this worktree never built.

What the ledger is NOT

It is not a duplicate of the manifest locks, and the two hashes are different by construction: the lock carries the file's raw content hash, the ledger a normalised one (state: PackAbsent on every entry since the duplicate Agent/ + Skill/ packages were consolidated away in 2026-08). So a green lock cannot imply a green ledger, and the ledger's core hash cannot be read off a manifest.lock.

Reading the red that tells you — a guard's self-test looks exactly like its verdict

A related trap, because it is how the same PR nearly mis-read a different guard. Most guards in scripts/ run their --self-test before their verdict, and the self-test drives deliberately broken fixtures — which print as ##[error] lines in the job log. Grepping the log for ##[error] and reading the first hit therefore quotes a FIXTURE. Measured 2026-09-19 on the resolver drift guard: the log carries 2 code line(s) differ from a fixture above the real verdict of 32 code line(s) differ (76 raw).

Ask the job which STEP failed instead — one call, and a fixture cannot forge it:

gh api "repos/Systemorph/MeshWeaver.Plugins/actions/jobs/<id>" \
  --jq '.steps[]|select(.conclusion=="failure")|.name'

Then read the log from that step. The step name is also the better thing to quote in a PR body or a handover, because it says which invariant broke rather than which number was largest.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.