Backtest: one replay tool for every detector - #371
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_3358e2f1-2285-4832-a5f3-d5735c9c7309) |
Three detectors each had a hand-rolled backtest.py. scripts/backtest_detector.py now replays any PATH:CALLABLE detector over local Claude Code and OMP transcripts, with --compare against another detector or a git ref. The hooks gain replay functions beside their unchanged decision code, and the three per-detector scripts are deleted. Carved out of #371 onto current main, with two fixes its tests needed there: - materialize_revision resolves symlinks before taking the path relative to the git toplevel. On macOS /var is a link to /private/var, so the relative path climbed out of the archived copy and --compare HEAD loaded the working tree instead of HEAD. - the near-miss test used a bare UNVERIFIED: line, which diu-stop no longer honours (#415); it now uses inline code excused by fenced output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gnua5cuAAF1ey1b7zRqJCC Change-Id: I3066797e7031407d5a7074b62a57d7e0e2db25b7
c96fbbe to
96cb317
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_f0bedb5e-532b-4d64-a4f3-d09af436478b) |
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_54a31aa6-8917-4606-9ef0-34e48a76e488) |
Three per-detector backtest.py scripts become one runner, scripts/backtest_detector.py, that replays any PATH:CALLABLE detector over local Claude Code and OMP transcripts and can compare it with an older copy of itself. Each hook gains a replay function beside its unchanged decision code. Refactor only: no hook's logic changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gnua5cuAAF1ey1b7zRqJCC Change-Id: I3834a0d53a47492861b9eb14b9fb4082a1283c5d
48c7792 to
43a02ea
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_4bcfa37a-7025-4660-8ff7-0b03e62d602e) |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sYZswUQ3HEnUEdVWrUbvq Change-Id: I0d463e2ea5c615efd64b715f204372d6922376c2
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_0d8979c0-25e4-4038-9951-34b16e794c61) |
Summary
One command now replays any detector over your real past chats and shows what it would have caught.
Before, three detectors each had their own replay script, and each worked its own way.
Now one shared script replays any of them. This is a refactor: no hook's logic changes.
Review Claim
Move Function: the three per-detector
backtest.pyscripts become one shared runner,scripts/backtest_detector.py, and each hook gains a replay function the runner calls, with no change to any hook's logic.Review Lane
refactor
Review Unit
engine-runtime
Safety Invariant
Refactor only: no hook's logic changes. wait-needs-wakeup's check was split into smaller functions so the replay tool can reuse them; its old tests pass on the new code (20 of 22; the other 2 tested the deleted replay script). repeat-error-stop only gains lines. Transcripts stay on the local machine; nothing from them is committed.
Slice Rationale
This PR used to carry the runner plus a gate that asked for its numbers. That gate watched regex patterns, which the phrase-list work is replacing, so it is dropped here and comes back later watching phrase lists instead.
The branch is now one commit on main. It previously sat on the phrase-list PR, which has merged, and carried that PR's commits as well; those are gone, so the diff is this PR's own sixteen files.
Non-goals
backtest.pyscripts get no wrappers; their READMEs andcost-audit.mdshow the runner command.Architecture
Before
graph TD T["local transcripts"] --> A["repeat-error-stop/backtest.py"] T --> B["wait-needs-wakeup/backtest.py"] T --> C["reflect/scripts/backtest.py"] A --> D1["repeat-error-stop detect.py"] B --> D2["wait-needs-wakeup detect.py"] C --> D3["token_audit.py frustration check"]After
graph TD T["local transcripts (Claude Code, OMP)"] --> R["scripts/backtest_detector.py"] R -->|"--detector PATH:CALLABLE"| D["replay functions: replay_blocks, replay_stop, pretooluse_reason, replay_frustration"] R -->|"--compare REF"| G["same detector at a git ref"] R --> N["hits, newly caught, newly missed"]Test Plan
Test Plan
Run on the rebuilt branch, one commit on main:
python3 -m unittest discover -s tests -p test_backtest_detector.py:Ran 20 tests ... OKpython3 -m unittest discover -s engine/hooks/repeat-error-stop/tests:Ran 32 tests ... OKpython3 -m unittest discover -s engine/hooks/wait-needs-wakeup/tests:Ran 25 tests ... OKpython3 -m unittest discover -s engine/skills/reflect/scripts/tests:Ran 180 tests ... OKuvx ruff check . --select E9,F(CI's lint):All checks passed!git patch-id --stablematches the old range's patch idengine/hooks/wait-needs-wakeup/tests/test_hooks.pyrun against this branch'sdetect.py, with an empty stand-in for the deletedbacktestmodule:Ran 22 tests,FAILED (errors=2), both errors inTestBacktestReproducesTheIncident, which tested the deleted script. The 20 tests of the hook's own decisions pass.python3 scripts/backtest_detector.py --detector engine/hooks/diu-stop/claude_stop_check.py:find_unverified_claim --limit 3: exit 0ast.parse(..., feature_version=(3, 9))) of the changed.pyfiles:problems: 0Two fixes were needed to move onto current code:
--compare HEADon macOS loaded the working tree instead ofHEAD:/varis a link to/private/var, so the path relative to the git toplevel climbed out of the archived copy.test_compare_against_git_ref_loads_that_revision_and_its_siblingsfailed before the fix and passes now.UNVERIFIED:line, which diu-stop no longer honours; it now uses inline code excused by fenced output.Revert Plan
Revert Plan
git revert <merge-sha>backtest.pyscripts come back.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gnua5cuAAF1ey1b7zRqJCC
Note
Low Risk
Refactor-focused: hook blocking logic is largely unchanged aside from small extractions for replay; new runner is not imported by production hooks yet.
Overview
Introduces
scripts/backtest_detector.py, a shared CLI that replays anyPATH:CALLABLEdetector over local Claude/OMP JSONL (discovery, streaming, hit rates,--json, and--compareagainst another detector or a git revision).Per-detector replay hooks are added or exposed instead of bespoke runners:
replay_blocks(repeat-error-stop),pretooluse_reason+replay_stop(wait-needs-wakeup, withWakeupTracker/stop_gapsextracted for reuse), andreplay_frustration(reflecttoken_audit, plusdirect_human_claude_rowsfor row streaming).wait-needs-wakeup/backtest.pyandreflect/scripts/backtest.py(and its unit tests) are removed; docs point at the shared runner. repeat-error-stop keeps its legacybacktest.pybut documents the new entry point.Tests: new
tests/test_backtest_detector.py(20 cases); hook tests updated for replay APIs (TestReplayBlocks,TestBacktestDetectors,TestReplayFrustration).Reviewed by Cursor Bugbot for commit 6b4ee38. Bugbot is set up for automated code reviews on this repo. Configure here.