Measured during #11451 (PR #11537), 2026-08-24. Filed by the domain:services execution seat. This is a defect in the ablation protocol itself, which every dev in this lane is briefed to follow, so it costs more than one card if it stays unrecorded.
What happened, in the dev's own words
RESTORE LEG: the trap's git checkout ran from the wrong cwd and DID NOT restore — caught because its own git hash-object echoed EMPTY. Restored by hand from the repo root and verified byte-identical (382ee3d6458e864cc81e8745ccdad57734bc88a5 == the HEAD blob), git status clean. Had I trusted the trap, every later measurement would have been taken on a mutated tree.
The ablation itself was exemplary — mutation confirmed on disk by anchored grep in both directions, direction predicted in writing before running, 85 passed unmutated → 9 failed ablated, exactly the predicted set. The restore is the leg that failed.
Why this is a protocol defect, not one dev's slip
The brief every os-dev in this lane receives says:
restore byte-identically (verify with git hash-object), and use trap restore EXIT INT TERM
Two instructions, and the ordering of trust between them is not stated. The trap reads as the mechanism and the hash check reads as belt-and-braces. It is the other way round. A trap firing proves only that the shell ran a function; it proves nothing about that function's effect. Here the trap fired, git checkout resolved a relative path against a cwd that was no longer the repo root, and the tree stayed mutated.
⚠️ The consequence is not a failed ablation — it is every subsequent measurement in the session silently taken on a mutated tree, reported as clean. That is the same class as the ablation hazards already recorded (green-under-ablation is NOT MEASURED; a zero-hit needs a positive control): an instrument that reports success while measuring the wrong thing.
The specific trap: an EMPTY hash is not a mismatch, it is a missing file
git hash-object <path> on a path that does not resolve produces empty output, not a differing hash. So a verification written as "did the restore command run" passes, and even a naive comparison can be written in a way that treats empty as "nothing to compare". The check must be:
- compare the post-restore
git hash-object against the HEAD blob hash for that path, and - treat an empty or absent hash as FAILURE, never as a pass, and
- confirm
git status is clean afterwards.
The dev caught it precisely because its restore function echoed the hash and a human-readable EMPTY was visible. A silent comparison would not have surfaced it.
Proposed hardening (not a ruling — this touches a governed surface)
- Absolute paths in the trap. Capture
REPO_ROOT="$(git rev-parse --show-toplevel)" before the mutation and restore against "$REPO_ROOT/<path>", so the restore cannot depend on cwd at trap time. - State the trust ordering explicitly in the brief: the
trap is a convenience for the crash path; the hash comparison is the proof. A trap that fired is not evidence of restoration. - Name the empty-hash case in the protocol text, since it is the failure mode that looks like success.
- Consider having the restore leg
exit non-zero and say so loudly when the hash does not match, so a session cannot continue measuring on a mutated tree.
⛔ The natural home for (2) and (3) is .claude/agents/os-dev.md and the pm-dispatch skill text — governed surfaces, maintainer-merge-only. Filing rather than fixing, and this seat will not open a PR against them.
Related, from the same report — worth folding into the same pass
Two more instrument-vs-premise traps the same dev hit and self-corrected:
- A package typecheck can say nothing about the test files you edited.
@objectstack/plugin-security's tsconfig excludes **/*.test.ts — verified with --listFiles (0 hits for the two edited test files). So "typecheck clean" would have been a true statement that did not cover the new code. It reproduced the TEST_DEBT ledger entry directly instead (11 raw errors, matching, none naming a touched file). MODULE_NOT_FOUND is not a red gate. Three gate invocations exited 1 on the dev's own wrong filenames; re-run via the real pnpm check:* commands. An exit 1 that never reached the gate body belongs with exit 99 / queue-timeout / PREREQUISITE NOT MET as NOT MEASURED, not as a failure.
Refs: #11451 / PR #11537 (where this was measured) · #11363 (verify-lock contention, the other standing measurement hazard) · #11463 (the never-park clause, same family: a clause that exists but loses the tiebreak at the moment of decision)
Measured during #11451 (PR #11537), 2026-08-24. Filed by the
domain:servicesexecution seat. This is a defect in the ablation protocol itself, which every dev in this lane is briefed to follow, so it costs more than one card if it stays unrecorded.What happened, in the dev's own words
The ablation itself was exemplary — mutation confirmed on disk by anchored grep in both directions, direction predicted in writing before running, 85 passed unmutated → 9 failed ablated, exactly the predicted set. The restore is the leg that failed.
Why this is a protocol defect, not one dev's slip
The brief every os-dev in this lane receives says:
Two instructions, and the ordering of trust between them is not stated. The
trapreads as the mechanism and the hash check reads as belt-and-braces. It is the other way round. Atrapfiring proves only that the shell ran a function; it proves nothing about that function's effect. Here the trap fired,git checkoutresolved a relative path against a cwd that was no longer the repo root, and the tree stayed mutated.The specific trap: an EMPTY hash is not a mismatch, it is a missing file
git hash-object <path>on a path that does not resolve produces empty output, not a differing hash. So a verification written as "did the restore command run" passes, and even a naive comparison can be written in a way that treats empty as "nothing to compare". The check must be:git hash-objectagainst the HEAD blob hash for that path, andgit statusis clean afterwards.The dev caught it precisely because its restore function echoed the hash and a human-readable EMPTY was visible. A silent comparison would not have surfaced it.
Proposed hardening (not a ruling — this touches a governed surface)
REPO_ROOT="$(git rev-parse --show-toplevel)"before the mutation and restore against"$REPO_ROOT/<path>", so the restore cannot depend on cwd at trap time.trapis a convenience for the crash path; the hash comparison is the proof. A trap that fired is not evidence of restoration.exitnon-zero and say so loudly when the hash does not match, so a session cannot continue measuring on a mutated tree.⛔ The natural home for (2) and (3) is
.claude/agents/os-dev.mdand the pm-dispatch skill text — governed surfaces, maintainer-merge-only. Filing rather than fixing, and this seat will not open a PR against them.Related, from the same report — worth folding into the same pass
Two more instrument-vs-premise traps the same dev hit and self-corrected:
@objectstack/plugin-security's tsconfig excludes**/*.test.ts— verified with--listFiles(0 hits for the two edited test files). So "typecheck clean" would have been a true statement that did not cover the new code. It reproduced theTEST_DEBTledger entry directly instead (11 raw errors, matching, none naming a touched file).MODULE_NOT_FOUNDis not a red gate. Three gate invocations exited 1 on the dev's own wrong filenames; re-run via the realpnpm check:*commands. An exit 1 that never reached the gate body belongs withexit 99/queue-timeout/PREREQUISITE NOT METas NOT MEASURED, not as a failure.Refs: #11451 / PR #11537 (where this was measured) · #11363 (verify-lock contention, the other standing measurement hazard) · #11463 (the never-park clause, same family: a clause that exists but loses the tiebreak at the moment of decision)