Skip to content

Require every blocking gate to prove it can fail (check-liveness ratchet) - #996

Merged
boshu2 merged 2 commits into
mainfrom
claude/check-liveness-sweep
Jul 26, 2026
Merged

Require every blocking gate to prove it can fail (check-liveness ratchet)#996
boshu2 merged 2 commits into
mainfrom
claude/check-liveness-sweep

Conversation

@boshu2

@boshu2 boshu2 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

What

A shrink-only ratchet requiring every blocking script-backed gate to have a test that demonstrates it failing on what it detects — plus the first ratchet-down, a real negative witness for skill.schema.

This is P3 from the 2026-07-25 review: fix the inert-check class, not just the two instances.

Why

PR #995 fixed two load-bearing rules that were green while broken — ADR-0016's "Python never ships in skills" with no gate, and an exact-identity digest contract whose test mocked Validate with the code under test's own digest function. Both were instances of one thing: a check nobody had ever proven executes.

A gate verified only against a green tree is indistinguishable from exit 0. Delete its detector and CI stays green.

Measured across the registry: 32 of 54 blocking script-backed gates had no test anywhere under tests/ demonstrating they can fail. 17 had no test at all — including skill.schema and skill.triggers, which gate every SKILL.md in the corpus.

The closure lives in Go because the registry does. My first measurement parsed seed.go from Python and undercounted by four — always.git-config-hygiene, always.retrieval-manifest-paths, contract.verdict-corpus, and workflow.install-drift are registered outside that file. The authority is gates.Default.All(), and reading it any other way reproduces exactly the projection-drift ADR-0016 is named after. The test mirrors the existing TestRatchetLibConsumersRouteLibEdits closure and is self-extending — a new gate is covered the moment it registers.

How it works

Same mechanism the repo already uses for preamble adoption and shipped Python:

  • Gates lacking a witness at the cutoff are pinned in scripts/.gate-negative-witness-grandfather and exempt — zero churn.
  • A new blocking gate must ship with a witness or the test fails.
  • A pinned gate that gains a witness must be pruned.
  • A pinned gate that leaves the registry must be pruned (dead weight hides the real count).
  • The growth guard reads HEAD, so a change cannot exempt its own new gate in the same diff.
  • The surviving count prints on every run, so the number is public and monotone.

"Negative witness" is deliberately generous about form — bats, shell integration tests, and Python returncode != 0 all count. The bar is somebody proved it can fail, not a particular framework. The teeth come from the growth guard, not from the regex being exhaustive.

How I tested

The meta-test's own liveness — all four failure directions seeded and observed, because a check-liveness ratchet that was only ever seen passing would be self-refuting:

Seeded Observed
Unpin a gate with no witness (skill.schema) FAIL, names the gate and its backing script
Pinned gate gains a witness FAIL demanding the prune
Pin a gate absent from the registry FAIL demanding the prune
Add an entry not present at HEAD FAIL — "the allowlist only SHRINKS"

The growth-guard direction was verified after the commit, since it correctly stands down while no HEAD version of the snapshot exists (the initial-snapshot state).

The ratchet-downtests/scripts/validate-skill-schema.bats, 5/5 green. Drives the real validator against a fixture tree (the script anchors REPO_ROOT to dirname $0/..) and asserts exit 1 for each missing required field. Count goes 32 → 31.

A fixture-fidelity trap worth recording: the first draft's negatives passed for the wrong reason. Descriptions in this corpus embed Triggers: "x", and unquoted that colon makes YAML read a nested mapping — so the gate failed on a parse error, not the schema violation each test claimed to witness. Caught only because the positive case failed too. Descriptions are now single-quoted, and the missing-name case is confirmed to fail on 'name' is a required property. A negative that fires for the wrong reason is not a witness.

Everything elsego build, go vet, full go test ./... green; the bats path-filter wiring tests (test-bats-path-filter-wiring, test-pathfilter-gate-coverage) green, so the new bats file is correctly routed; shellcheck -S warning clean; make regen-check clean.

Not checked: the 31 gates still on the allowlist remain unproven — that is the point of the ratchet, not an oversight. They are now counted and can only decrease.

Checklist

  • make build && make test passes (if Go changes)
  • No secrets or credentials in code
  • Breaking changes documented (if any) — none. The ratchet is grandfathered to the current tree and causes zero churn; only new blocking gates are constrained.

Generated by Claude Code

The 2026-07-25 review found two load-bearing rules that were green while
broken: ADR-0016's "Python never ships in skills" with no gate, and an
exact-identity digest contract whose test mocked Validate with the code under
test's own digest function. PR #995 fixed both instances. This is the class.

A gate verified only against a green tree is indistinguishable from `exit 0` --
delete its detector and CI stays green. Measured across the registry: 32 of 54
BLOCKING script-backed gates had no test anywhere under tests/ demonstrating
they FAIL on what they claim to detect. 17 had no test at all, including
skill.schema and skill.triggers, which gate every SKILL.md in the corpus.

The closure lives in Go because the registry does. An earlier bash/Python
projection of seed.go missed four gates registered outside that file
(always.git-config-hygiene, always.retrieval-manifest-paths,
contract.verdict-corpus, workflow.install-drift) -- the authority is
gates.Default.All(), and reading it any other way reproduces exactly the
projection-drift ADR-0016 is named after. It mirrors the existing
TestRatchetLibConsumersRouteLibEdits closure and is self-extending.

Mechanism is the repo's proven shrink-only ratchet: the gates lacking a witness
are pinned in scripts/.gate-negative-witness-grandfather; a NEW blocking gate
must ship with a witness; a pinned gate that gains one must be pruned; a pinned
gate that leaves the registry must be pruned; and the growth guard reads HEAD,
so a change cannot exempt its own new gate in the same diff. The surviving
count prints on every run.

Ratcheted down by one immediately, to prove the mechanism moves rather than
just counts: tests/scripts/validate-skill-schema.bats is a real witness for
skill.schema (32 -> 31). It drives the actual validator against a fixture tree
and asserts exit 1 on each missing required field.

One fixture-fidelity trap worth recording: the first draft's negatives passed
for the WRONG reason. Descriptions in this corpus embed `Triggers: "x"`, and
unquoted that colon makes YAML read a nested mapping -- so the gate failed on a
parse error, not the schema violation each test claimed to witness. Caught
because the positive case failed too. Descriptions are now single-quoted and
the missing-name case is confirmed to fail on "'name' is a required property".

Verified: the meta-test's own three failure directions each seeded and observed
(unpin a witness-less gate -> FAIL; pin a gate that gained a witness -> FAIL
demanding the prune; pin a gate absent from the registry -> FAIL); 5/5 bats on
the new witness; go build, go vet, full go test ./... green; the bats
path-filter wiring tests green; make regen-check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJNsMBvvKCYktzaFo3E6Fu
… test

go.test-isolation caught the new test: its git-exec env-scrub ratchet rose from
3 to 4 because negative_witness_test.go execs `git show` with cmd.Dir set but
no cmd.Env. Relying on TestMain's process-level ScrubGitDiscoveryEnv was not
enough — the repo's contract is a per-command scrub, and it should not depend on
a TestMain elsewhere in the package staying correct. Under a hook-leaked GIT_DIR
even a directory-scoped git call resolves against the LEAKED repository
(age-gate-scripts-worktree-gitdir-p62wo, ek8v).

Adds a package-local scrubbedGitEnv twin of gitDiscoveryEnv and wires it into
the one git call. Ratchet is back at baseline 3 with this file no longer listed.

Worth recording for the PR's own subject matter: go.test-isolation is on the
check-liveness allowlist as lacking a proven negative — and it just caught a
real defect anyway. The pin measures TEST COVERAGE of a gate, not its efficacy.
An unproven gate may work fine; the point is that nothing would tell us if it
stopped.

Verified: check-test-isolation.sh PASS (git-unscrubbed=3/3); full go test ./...
green; meta-test green at 31; growth guard still fires on a seeded self-allowlist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJNsMBvvKCYktzaFo3E6Fu
@boshu2
boshu2 merged commit f66ab95 into main Jul 26, 2026
7 checks passed
@boshu2
boshu2 deleted the claude/check-liveness-sweep branch July 26, 2026 14:31
boshu2 added a commit that referenced this pull request Jul 28, 2026
…to fail

Under set -euo pipefail a '! grep' line is a silent no-op — errexit never
fires for !-inverted pipelines — so the forbidden-vocabulary guards in the
rpi, plan, implement, learn, ms, scaffold, and security validators could
never fail their script. Witnessed RED (seeded tokens, all seven exited 0),
converted each to the if-grep-exit-1 house form, and reworded learn's
boundary sentence whose own denial ('emit a lifecycle receipt') trips the
now-live guard. tests/scripts/skill-validator-liveness.bats pins the class:
a static no-inert-negation sweep plus seeded behavioral liveness per skill.

W1 of age-skill-overhaul-reboot-sjv7v (#996 doctrine applied to skill
validators).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants