hook: backstop layer catches a stale verdict without needing the model to notice - #315
Conversation
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_70ba225e-6073-40f3-952d-e4d363e88fce) |
d72660d to
b7a1dbf
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_767c2c99-8656-4358-a259-baec1ea52d17) |
e8b239b to
7189166
Compare
b7a1dbf to
7b08a99
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_96efb93e-0d91-470a-a6ef-a38bb3dbf84d) |
7189166 to
d2f72c2
Compare
7b08a99 to
e69293e
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_bfe3801e-06d2-44ae-af81-1c23143242b9) |
d2f72c2 to
ef8555e
Compare
e69293e to
6a2cb08
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_ba363493-2449-4a5c-ad2a-118915434c30) |
ef8555e to
419a633
Compare
…l to notice
- Problem: the two self-correction layers below this both depend on the model.
wrong-check-reflect matches the shape of a retraction in the outgoing text,
so it only helps once the model has already decided to admit something.
principle-flag-your-own-corrections carries the judgment no regex can
enumerate, but still needs the model to realise a claim went stale. Neither
catches the silent switch to the corrected value -- which is the failure
principle-flag-your-own-corrections itself names, because the user cannot
tell a silent correction from consistency.
- Trigger: user asked for "another check underneath the nondeterministic layer
just to be absolutely certain the AI didn't miss anything."
- The live case: check_skill_test_coverage.py printed ok for a stacked slice it
never compared, that was reported as "fully green", and the same script
failed once given the slice refs. Nothing mechanical connected the two runs.
- Chose transcript evidence over text matching. The hook pairs Bash tool_use
blocks with their tool_result output, normalises each command to the thing
being verified, and notes a target whose verdict went pass -> fail. That is
a matter of record, so it needs neither a phrasing match nor the model
noticing. Third layer, weakest dependency: none.
- Chose pass -> fail only. fail -> pass is ordinary repair work; firing there
would punish fixing things (fixture `fixed.jsonl`).
- Chose to scope tracking to verifier-shaped commands (check_/test_/run_,
pytest, unittest, npm/pnpm test, cargo/go test, make test, jest, vitest,
preflight). An `ls` that succeeds then fails is not a stale verdict
(fixture `noise.jsonl`).
- fail wins over pass when one output contains both, because a run prints ok
lines for early gates and can still fail overall -- exactly the preflight
output shape in this repo.
- Chose advisory (stderr, exit 0) rather than blocking. A gate can
legitimately start failing because the turn broke it on purpose, and the
hook cannot know intent. Once per transcript per target.
- Extended the scenario runner so a `ran` entry may be {cmd, output}. This
hook compares two runs of the same verifier, so command strings alone could
not drive it.
- Advisory only: never blocks a turn, never edits anything.
- New: engine/hooks/verdict-flip-watch/ (detect.py, claude_stop_check.py,
claude.hook.json, install_claude_hook.py, README.md, 4 transcript fixtures,
20 tests); tests/scenarios/verdict-flip.json.
- Modified: install.sh (link + installer line, following hedge-runs-prove-it),
scripts/run_skill_scenarios.py (command output support).
- New state dir under ~/.cache/catstack-verdict-flip-watch for
once-per-target suppression. Overridable via VERDICT_FLIP_WATCH_STATE_DIR,
which the tests use so runs do not leak into each other.
- Revertable with git revert; rerun ./install.sh to drop the hook entry.
- python3 -m unittest discover -s engine/hooks/verdict-flip-watch/tests
-> Ran 20 tests, OK
- python3 scripts/check_hook_test_coverage.py engine/hooks/verdict-flip-watch
-> check_hook_test_coverage: OK (1 hook(s) checked)
- python3 scripts/run_skill_scenarios.py -> ok all 17 scenario(s) behaved as
declared. The decisive one is stale-green-caught-without-any-admission:
verdict-flip-watch FIRES while wrong-check-reflect stays SILENT on the same
transcript, which is the layering this slice exists to add.
- python3 engine/skills/make-pr/scripts/preflight.py --base
admission-scenarios -> ok preflight passed
- Gates: ecosystem boundaries, skill file refs, three-harness, trigger policy,
subagent scope contract -> all ok
- python3 -m unittest tests.test_install -> Ran 58 tests in 280.443s, OK
(exit code 0). This exceeds the 120s foreground command timeout, so it
completed in the background; an earlier read of the output file happened
before it finished and this commit first recorded it as NOT RUN. Corrected
here: it ran and passed. The two install.sh lines are at install.sh:239 and
install.sh:335.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQMWSLRArEfEm1psa7RKdD
…trings ruff E9,F flagged `io` and `unittest.mock.patch` as unused in the verdict-flip-watch hook tests, and the no-comments gate flagged four comment lines added to its detector. The two comment blocks explained what VERIFIER_RE and ACKNOWLEDGED_RE are for. That reasoning moves verbatim into the module docstring, which the gate exempts, so nothing is lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013mDsvDgGuwaBuyRtYgvktd
6a2cb08 to
3b1399f
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_4dd98df1-ac58-4cd9-ad92-39899d9cfb8c) |
Summary
The two layers below this one both depend on the model.
wrong-check-reflectmatches the shape of a retraction in the outgoing text, so it only helps once the model has decided to admit something.principle-flag-your-own-correctionscarries the judgment no regex can enumerate, but still needs the model to realise a claim went stale.Neither catches the silent switch to the corrected value — which is the failure the principle itself names, because the user cannot tell a silent correction from consistency.
This hook needs neither. It reads the transcript's own commands and their results. If a verifier printed
okearlier and failed later, the earlier status is stale as a matter of record.Review Claim
A stale verdict should be caught from transcript evidence, independently of whether the model admits it or phrases the admission in a recognised way.
Review Lane
behaviorReview Unit
engine-runtimeSafety Invariant
Advisory only: stderr plus exit 0. It never blocks a turn, edits a file, or runs a command. Suppressed once per transcript per target, and silent whenever the outgoing message already names the flip. Fail-open on any parse or IO error, and silent with no transcript path. The only new state is a cache dir for that suppression, overridable by env var.
Slice Rationale
Last in the stack because it depends on both layers below being in place — its decisive scenario asserts that it fires while
wrong-check-reflectstays silent on the same transcript, which needs that hook's current behavior to compare against.Kept separate from the detector widening (#309) because that slice changes an existing hook's matching and this adds a new hook with a different mechanism. A reviewer judging "is this regex too broad" and one judging "is transcript-evidence the right backstop" are answering different questions.
Non-goals
fail→pass. That is ordinary repair work; firing there would punish fixing things.lsthat succeeds then fails is not a stale verdict.wrong-check-reflector the corrections principle.Test Plan
Test Plan
python3 -m unittest discover -s engine/hooks/verdict-flip-watch/tests→Ran 20 tests ... OKpython3 scripts/check_hook_test_coverage.py engine/hooks/verdict-flip-watch→OK (1 hook(s) checked)python3 scripts/run_skill_scenarios.py→ok all 17 scenario(s) behaved as declaredpython3 engine/skills/make-pr/scripts/preflight.py --base admission-scenarios→ok preflight passedokThe decisive scenario is
stale-green-caught-without-any-admission: on one transcript,verdict-flip-watchfires whilewrong-check-reflectstays silent. That is the layering this slice adds, asserted rather than described.Four transcript fixtures cover the boundaries: the real pass→fail case, a verifier that stays green,
fail→pass(a fix), and a flip on a non-verifier command.python3 -m unittest tests.test_install→Ran 58 tests in 280.443s ... OK(exit 0)That last one exceeds the 120s foreground command timeout, so it finished in the background. This PR body and the commit message first recorded it as not run, because the output file was read before the run completed. Corrected: it ran and passed. The wiring lines are at
install.sh:239andinstall.sh:335, following thehedge-runs-prove-itpattern.Revert Plan
Revert Plan
git revert <sha>./install.shfrom the main checkout to drop the hook entry from~/.claude/settings.json~/.cache/catstack-verdict-flip-watchcan be deleted at will.🤖 Generated with Claude Code
https://claude.ai/code/session_01JQMWSLRArEfEm1psa7RKdD
Note
Low Risk
Advisory-only Stop hook (exit 0, stderr); no blocking or file edits, fail-open on errors, with tests and install wiring matching existing hook patterns.
Overview
Adds
verdict-flip-watch, a third advisory Claude Stop hook that flags when the same verifier passed earlier in the transcript and failed later while the outgoing reply does not acknowledge the stale earlier green.Detection is transcript-driven: it pairs Bash
tool_usecommands withtool_resultoutput, normalizes verifier targets (check_*, test runners, etc.), classifies pass/fail (fail wins in mixed output), and emits a one-time stderr nudge per transcript/target unless correction vocabulary is already in the message. It is wired throughinstall.sh(symlink +install_claude_hook.py), with unit fixtures/tests and hook coverage checks.run_skill_scenarios.pynow allowsranentries as{cmd, output}so scenarios can simulate real tool results;tests/scenarios/verdict-flip.jsonasserts the hook fires on silent stale-green (whilewrong-check-reflectstays silent), stays quiet when the reply owns the flip, and ignores fail→pass repair runs.Reviewed by Cursor Bugbot for commit 3b1399f. Bugbot is set up for automated code reviews on this repo. Configure here.