Repository navigation
Redraft checks that pass before the work; discrimination gate opt-in - #34
solvyxtech wants to merge 4 commits into
Conversation
solvyxtech
left a comment
There was a problem hiding this comment.
Grok review of #34: changes requested (posted as a comment because GitHub won't let this shared account request changes on its own PR)
I reviewed the PR's own commit (b1a56cd, 18 files) by itself. The other 8 commits on this branch belong to #32 and the stack below it. I ran typecheck, build, and the touched TS suites (discriminating-checks, exit-codes, tiers, independent-checks, review-advisory, judgment, harness) on Linux, where /bin/sh is dash: 133 of 134 pass, and one fails (#1 below).
Pairing rules: all clear
- It adds no subscription backends and doesn't touch the removed Claude Code / agy / Gemini CLI paths.
- No competitor names appear in the added product copy. The model ids in the tests and in
discriminating_replay.pyare fixtures and bench lane data. finetune/andextract.mjsare untouched, and the extractor doesn't readtier, so the Grok exclusion is unaffected.- The branch is
claude/, it has theclaudelabel, and it targets a stack branch, notmain. 👍
Must fix
- New test fails on Linux.
a check that could not run before the work has no pre-work tryusessh tool.sh. On macOSshis bash, and a missing file exits 127, sodidNotRunmakes it broken and therefore untried. On Debian/Ubuntushis dash, which exits 2, so the pre-work try records it as failed and the turn endsverified / verified. The ubuntu leg of the CI matrix will hit this. Usebash tool.sh(or./tool.sh). It also shows that the "untried" bucket depends on the shell's exit codes. - The pre-work try runs under a different shell than the bar.
preflightCriteriausesdraftedShellonly whenstrayis set. Neither the up-front call insealCriterianortryLateBeforeWorkpassesstray, so drafted checks are tried before the work under/bin/shbut run at the bar under bash. Before this PR the pre-work result only relabelled guards. Now it decides "verified". A bashism such as[[ ]],<( ),sourceorset -o pipefailcan fail under dash for reasons that have nothing to do with the work. Mostly that lands in broken/untried, which fails closed and loses right verifieds. But anything that exits non-127 under dash and passes under bash counts as discriminating, which fails open. Pick the shell from the check (draftedShell(c)), not fromstray. - The worker can reach the pre-work copy during the work.
preWorkTreelives in$TMPDIR/maat-check-*from turn start until the claim, and late checks are tried against it. A worker with a shell, running as the same user (the default, and every bench container), can empty or edit that copy. That turns a check that would have passed on the pristine tree into "failed before the work", and the turn earns "verified". Up-front tries don't have this problem because they run before the worker starts. A cheap fix: record a digest (file list plus sizes/mtimes, or a hash) when the copy is made, and treat late checks as untried if it changed. Also keep the copy out of the worker's reach under--worker-user(#29).
Should fix
- Late checks that use absolute project paths or linked dirs hit the live, finished tree.
copyTreesymlinksnode_modules,.venv,venvand the rest back to the project, and nothing rewrites an absolute path like/app/...(Terminal-Bench's cwd). A late check like that, tried at claim time, sees the finished work and passes, so it's labelled a guard and a right verified is denied. That fails closed, but it will undercount. Consider marking a late check untried when its command names the absolute project root. - Docs and CHANGELOG. "Verified" now needs a check that failed before the work, in every mode. The replay drops today's lanes from 22 verifieds to 10, so this changes what users see and moves many
--criteria auto --judgeruns from exit 0 to exit 3. Please add aCHANGELOG.mdentry and update the README's verified / Terminal-Bench wording andbench/harbor/README.md. This is the same ask as on #32. - Base and mergeability. The PR targets
claude/sync-local-work, so GitHub shows 9 commits / 47 files, it's conflicting (mergeable_state: dirty), and no checks ran. Retarget toclaude/independent-verified(#32) so the diff is the one commit, and get CI green. - Carried over from #32, still open there. With
--review-advisory,src/cli.tsxandui/app.tscheckoutcome === "verified" && claimbefore the unconfirmed and unreviewed branches. So an advisory run that a reviewer contradicted still prints a plainverified (independent checks: …). This PR leaves that order as it was. Fix it in #32 and the fix will flow into this branch.
Nits / notes
tryLateBeforeWorksetsthis.runningwithout checkingcancelRequested, unlikerunBarGuarded. A ctrl+C right before the claim waits out the late tries (up to 5s each). Mirrorif (this.cancelRequested) this.running.abort().preflightCriteriacopies its cwd again for each check, so late tries copy the copy. That's harmless, but it's an extra full tree copy per late check.- Every
--criteria autoturn with pending drafts now does a synchronouscopyTreeat turn start (up to 20k files / 512 MB). A timing line in the journal would help. - The PR body says
run.pyreadspassed-untested, butrun.pyisn't changed.claim_ofalready passes the claim through, as the new Python test confirms, so only the description needs fixing. noteCoverageis display-only and labelled that way on the receipt. Good.- pytest isn't installed on my box, so I didn't run
test_claim_labels.py. The logic reads fine.
The rule itself is right. A check that passes on the untouched tree can't vouch for the work, and enforcing that in every mode, not only advisory, is the correct call. The pre-work-try journal note and the receipt line are good additions. I'm happy to approve once 1–3 are fixed.
|
Rework after the coordinator's no-ship call (b5147cf). The discrimination gate is now opt-in, and the default fix moves to seal time: drafted checks that pass before the work, or cannot fail, are redrafted once and then dropped. Gate replay (
Seal-time screen replay (
The P1 verdicts come from running on this Mac rather than the bench container, with ports 808x moved because a listener on 8080 here would otherwise answer. These lanes were still running when the replays were taken, so counts can shift slightly. |
b5147cf to
0cd5326
Compare
|
rebased onto claude/independent-verified@064049f, CI green |
solvyxtech
left a comment
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
(Posted as a comment review: GitHub won't let this shared account request changes on its own PR.)
Fresh review at head 0cd5326. Commit 1 (18cfb6d) is the commit I reviewed earlier, rebased and moved to the async copyTree. Commit 2 (0cd5326) is new: the seal-time screen (P1 plus L16), with the gate made opt-in. I checked whether this actually closes the weak-judge hole, assuming a hostile worker.
Status of my 21:42 UTC review:
- The Linux-only test failure is still there.
- The
/bin/shvs bash split is unfixed, and it now also affects the default seal-time screen. - The pre-work copy in
/tmpis still reachable by the worker.
Checks run:
- Typecheck is clean.
- Full unit suite on Linux with node 20.19: 2058/2059. The one failure is item 3 below.
pytest bench/local/tests: 27 passed, 1 skipped.
Pairing terms: clean.
Blocking
1. A check that prints a boolean passes every new guard and earns "verified" on wrong work. src/checklint.ts:389/406, src/criteria.ts:368. High confidence: reproduced on this head.
- The check:
python3 -c "import csv; print(list(csv.reader(open('out.csv'))) == [['a'],['b']])".- L16
cannotFailreturns null, because there is no PASS/FAIL wording. evidenceTagstags ittask, value.- On the pristine tree it raises FileNotFoundError, so the pre-work try puts it in
failed. P1 keeps it, and the receipt says "before the work: failed (this check can tell the work from none)". - After wrong work (rows reversed) it prints
Falseand exits 0, so it passes. It passes the opt-in--require-discriminatinggate too.
- L16
- Why it matters: this is the csv-clean class the PR says it closes. Only the PASS/FAIL spelling is caught.
True/False,ok/badandecho $?tails all get through. With a separate judge (#32) the run reads "verified (independent checks: …)". - Fix: at bar time, treat a drafted check that exits 0 and whose last stdout line is
False,false,FAIL,FAILEDorNOas failed or undetermined. That closes the class however the check was drafted. Extend L16 toprint(<comparison>)with no exit or assert.
2. Pre-work tries run under /bin/sh; the final check runs under bash. src/criteria.ts:346. High confidence: reproduced.
- Why:
draftedShell({ hidden: !!opts.stray, … })chooses bash only whenstrayis set. None of the three callers that feed the new logic sets it: the screen (criteria.ts:1039), late checks (engine.ts:3720) and the seal (engine.ts:5479). - Effect: on Debian or Ubuntu, which includes the bench containers and CI,
/bin/shis dash. On a pristine tree,[ "$(cat in.txt)" == "a" ]andtest "$(echo -e x)" = xare failed under the screen but passed under bash (strayset). So a check that passes before the work survives P1, gets stamped as discriminating, and counts under the gate. - The replays won't show it:
prework_screen_replay.pyran on the Mac under bash, whereshis bash. - Fix: run each try under the shell the bar will use, i.e.
draftedShell(c)per check.
3. The sh tool.sh test still fails on Linux, and the behaviour differs by platform. test/discriminating-checks.test.ts:157. High confidence: reproduced.
- What happens: dash exits 2 for a missing script, not 127, so the try records "failed" and the turn ends
['verified','verified']. The test expectsunverified/passed-untested. CI on ubuntu-latest will go red. - Why it matters beyond the test: the same check is "not tried" on the Mac but "discriminating" on Linux. Item 2's fix is the same fix here; then add a Linux-safe case.
4. The pre-work copies can be reached and modified by the worker in the late path. src/cli.tsx:1243, src/engine.ts:5404, src/scratch.ts:188. Medium confidence; it needs the late path and the same uid.
- Where the copies live: both copies are
mkdtemp(tmpdir()), owned by the uid the worker's bash runs as. In the bench, Maat and the worker are bothagent. - When the worker can write: once the criteria wait seals what's ready (or nothing) and the draft keeps going as a late draft, the worker may write. The CLI copy stays alive until
autoDraftreturns; the engine copy stays alive for the whole turn. - Failure scenario: the worker deletes
in.txtinside/tmp/maat-check-*/…. A late check that passes on the real pristine tree now "fails before the work". P1 doesn't drop it, the receipt calls it discriminating, and the gate accepts it. - Fix: fingerprint the copy when it is created and re-check it before each try, treating a mismatch as "untried". Or keep the copy under Maat's private state dir with #29's
--worker-user.
Non-blocking
- A timeout counts as "failed before the work".
src/criteria.ts:347/368. High confidence from reading the code. The default try timeout is 5 s. Any check slower than that which passes on the pristine tree, such as an existing pytest suite, is put infailed. It survives P1 and is labelled discriminating. Treat a timeout as untried. - L16 false positive. Medium confidence: verified.
python3 -c "print('PASS' if … else 'FAIL')" | grep -qx PASSis flagged as unable to fail, butgrepmakes it fail. That drops a good check. - L16 false negative. Medium-low confidence.
grep -q 42 out.txt; echo $?always exits 0 and isn't flagged. - Screening runs drafted check commands while the worker can run read-only bash. Medium confidence.
isReadOnlyCommandacceptscat /proc/*/cmdlineandgrep -a . /proc/[0-9]*/cmdlinebefore the seal, and a check being tried shows up inbash -c '<check>''s cmdline. The class predates this PR (#27 follow-up), but the screen widens the window. Consider refusing/procreads from the worker before the seal, or running tries with a masked cmdline, e.g. writing the check to a 0700 temp file and running that file. - No journal note when the screen can't run. Low confidence. When
copyTreereturns null, the screen is skipped silently, so a replay can't tell that P1 never ran. - Inherited from #32: the advisory-mode "verified" label, and the undocumented exit 3. Please also add
--require-discriminating, thepassed-untestedtier and the P1 redraft or drop to the README and CHANGELOG.
Nothing merged.
|
The hidden-check argv leak (command visible in When #34 is rebased, its before-work tries — the 'check that failed before the work started' runs — are another hidden-command spawn and need |
On the 2026-10-07 container bench a separate judge drafted checks like `python3 server.py 8080 & sleep 1; curl -f .../items || echo 'fail'`. They exit 0 whatever happens, so they passed on the tree before server.py existed, and the run was labelled verified; the grader got a non-JSON 404 for GET /items/99. The receipt already said "pass (nothing to establish)" for every one of them, and the tier ignored it. A passing independent runs+value check now earns "verified" only when it FAILED on the tree as it was before the work (the pre-work try in sealCriteria). One that passed then too, could not run then, or joined later with no pre-work try guards against a regression and does not count. Otherwise the tier is passed-untested: "passed checks that did not test this work, not verified", outcome unverified, exit code 3. A person's check still verifies as before. Checks that join late (drafts past the time cut, the reference check) are tried against a copy of the project taken at turn start, so they get the same pre-work try. The try's outcome is journalled (pre-work-try), each receipt row says how the check fared before the work, and each "Recorded, not verified" note says whether a discriminating check plausibly covers it (word match, display only). bench/local/discriminating_replay.py recomputes the tier offline from the saved journals and receipts. Over 2026-10-07's lanes it removes 2 of the 4 wrong verifieds left after independence (q235 http-json-server, ml4b config-migrate) and denies 10 right ones; 8 of those rested only on checks that passed on the untouched tree (`|| echo fail`, `find -exec` exit codes) and 2 on late checks this build could not try.
The gate from the previous commit is now opt-in (--require-discriminating, MAAT_REQUIRE_DISCRIMINATING=1; --review-advisory implies it). Replayed over 2026-10-07's lanes it cost more right verifieds than it removed wrong ones, so default tiering is #32's again. The pre-work try is still journalled and shown on each receipt row, late checks are still tried on the turn-start copy, and the coverage notes stay. The cause is fixed where the checks are drafted instead. --criteria auto copies the project before the first step, and each drafted check is tried on that copy before it is sealed. One that already passes there cannot show the task was done: it goes back to the drafter once with "this check passes before any work, so it cannot show the task was done; make it fail on the current tree", and is dropped if the redraft still passes. Checks that cannot fail by construction get the same treatment through an always-on lint (L16): a trailing `|| echo ...`, `|| true`, `; exit 0`, `find ... -exec ... \;` (find ignores what -exec returns), PASS/FAIL printed with no failing exit, and, at the try, FAIL printed with exit 0. Drops are journalled as check-lint-dropped with rules P1-passes-before-work and L16-*. bench/local/prework_screen_replay.py runs today's 514 sealed drafted checks against their tasks' pristine trees: 112 would have been sent back (63 by the lint, 49 passing before the work). Of the 29 independent verifieds, 15 rested only on such checks: 3 of the 4 wrong ones (q235 http-json-server, q235 csv-clean, ml4b config-migrate) and 12 right ones, which would be judged by the redraft instead.
…ks under the bar's shell A check like `python3 -c "print(rows == expected)"` exits 0 whether it prints True or False. It passed the L16 lint and the pre-work screen (it raised before the work, so it "failed" there), and after wrong work it printed False, exited 0 and earned "verified". Two layers now: - L16 reads the step that decides the exit (top-level parse: quotes, heredocs, subshells and [[ ]] kept whole) and flags inline programs that print a comparison or boolean with nothing that exits non-zero, jq without -e, awk with no exit, a `; echo` / `; echo $?` tail without set -e or exit 1, and an `if ... fi` with no failing branch. Output read by a later pipe stage (`... | grep -qx PASS`) is no longer flagged. - At the bar and in the pre-work try, a task check whose deciding step printed its verdict (verdictPrinter) and whose last line is False, FAIL, FAILED or NO, or whose `echo $?` printed non-zero, has failed. jq's own output inside `find -exec` or `jq -e` is not read as a verdict. The pre-work try now runs each check under the shell the bar runs it with (draftedShell per check). It used /bin/sh unless `stray` was set, so on dash a bashism failed before the work and passed at the bar, and counted as discriminating. dash's exit 2 for a missing `sh script` reads like bash's 127, and a try that timed out counts as not tried. Replay (prework_screen_replay.py, today's lanes, 549 sealed checks): L16 static 65 -> 110, P1 53 -> 46, redrafted 118 -> 156. The new flags are all print-only checks. On the 31 independent verifieds, the bar-time read and the golden-file rule change no verdict; the wrong csv-clean verified rested on a PASS/FAIL print that L16 already sent back.
The copy taken at turn start for late checks lived in $TMPDIR, owned by the uid the worker's bash runs as. A worker could edit it so a check that passes on the pristine tree "failed before the work" and counted as discriminating. preWorkCopy takes a content digest when the copy is made (0700 parent; under --worker-user that is Maat's private state dir) and checks it before and after every late try and every seal-time screen. A copy that changed counts as not tried, journalled with tampered: true. A late check that names the project's absolute path reads the finished tree, so it is not tried either. Golden files a late check diffs against are recorded from the copy, so the #32 golden rule covers late checks too. The late try now honours a pending cancel, the copy's size and timing are journalled (pre-work-copy), and a missing copy is journalled instead of skipped silently. CHANGELOG, README and bench/harbor/README.md describe --require-discriminating, passed-untested, the P1 redraft and drop, the verdict read and the shell change.
0cd5326 to
8d0303c
Compare
|
Rebased onto #32's new head ( Checks run
Round 2 (00:11 UTC)Blocking 1: a check that prints a boolean earns "verified" on wrong work. Fixed in Approach. I considered the option of also requiring each value check to fail on a deliberately broken copy of the finished tree. I didn't adopt it: no cheap mutation is safe in general. Emptying the deliverable's files makes a correct absence check (
Tests
Blocking 2: the
Blocking 3: the
Blocking 4: the worker can modify the pre-work copies. Fixed in
Non-blocking, round 2
Round 1 (21:42 UTC)
Nits:
Also from the bug-hunt (on #32, carried into this branch)Golden files now must predate the work. Late checks get their pre-work record from the copy, and only for files inside it. An absolute golden path outside the project is read as it is now, so it never counts. Replay
That is expected: it models only the opt-in gate.
These P1 numbers come from macOS bash. With the shell fix, the bench container now tries checks under bash too, so the replay and the bench agree on the shell. |
Builds on #32 (claude/independent-verified): this branch starts from it, and the diff includes #32's commit until that merges.
The leak
Measured 2026-10-07 with Qwen3-235B as the worker and qwen3-coder-30b-a3b as a separate judge.
python3 server.py 8080 & sleep 1; curl -f .../items || echo 'fail'and two more like it. These exit 0 whatever happens, so they passed before server.py existed. The receipt already marked thempass (nothing to establish), but the run was still labelled verified, and the grader got a non-JSON 404.print('PASS' if ... else 'FAIL')always exits 0, and nothing checked row order.What this PR does
1. A seal-time screen, on by default (
--criteria auto). It fixes the cause where the checks are drafted.checklint.ts cannotFail), catches:|| echo …,|| true, or; exit 0;find … -exec … \;, because find's exit status ignores what -exec ran;check-lint-dropped, with ruleP1-passes-before-workorL16-*.2. The discrimination gate is opt-in (
--require-discriminating/MAAT_REQUIRE_DISCRIMINATING=1;--review-advisoryimplies it).passed-untested, exit code 3.3. Kept on in every mode:
pre-work-tryjournal note;before the work: failed / passed / not triedline on each receipt row;Measured
The numbers were taken at one point in time while today's lanes were still running, so they can shift slightly.
bench/local/discriminating_replay.pyreplays the opt-in gate over today's lanes (q235, mm3, ml4b, hk55, dsv4, gh, ga3b, gx). Each cell is right / verified:bench/local/prework_screen_replay.pyruns today's 514 sealed drafted checks against their tasks' pristine trees (bench setup), on this Mac, under bash.Expected effect:
Tests
test/prework-screen.test.tscovers the lint cases, the redraft-once behaviour, dropping a redraft that still passes, PASS/FAIL detection, and the case with no pre-work copy.test/discriminating-checks.test.tscovers the opt-in gate (the http-json-server case, failed before then passes after, no pre-work try), the default being unchanged, and the flag.test/exit-codes.test.tschecks that a drafted check that only passes before the work is redrafted, then dropped, and the run exits 3.npm run checkpasses: 2018 tests, plus e2e and self-check.