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 connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.