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.Test — a 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.

Adding a skill costs a THIRD thing, and it is a literal

An edit moves the two above. An addition also has to be NAMED in SKILL_MASTER_ROSTER (scripts/validate-repos.py): check_one_skill_master refuses a package re-shipping a platform default by matching file stems against that literal set, so a master the set does not name is a stem the gate walks straight past — a package could re-ship it while the lane stays green. Since the review of Plugins#2088 the folder is compared against the set in BOTH directions, so the omission is loud (validate-repos.py and its --self-test both go red naming the stem), but it is a third file in a third mechanism, and nothing about writing a markdown file suggests a Python set has to move.

Measured on Plugins#2210 (2026-09-21): Data/Skill/query.md landed with none of the three. The locks were stale on the branch's 34-commit-old base (branch-scale, fixed by merging main — the post-merge hook regenerated 19 of them), the ledger had no entry, and the roster did not name query; four required contexts were red on a diff of one markdown file. So for an ADDED skill the denominator is roster + ledger entry + locks, and the roster is the one no test in src/ runs.

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.