Repository navigation
Verified needs a check the worker did not write - #32
solvyxtech wants to merge 3 commits into
Conversation
solvyxtech
left a comment
There was a problem hiding this comment.
Grok review of #32: 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 (fc5aa1d, 27 files) on its own. The other 7 commits on this branch belong to #24–#27 and #30.
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 product copy. The vendor ids in
normalizeModelIdare code, and the ones intest/independent-checks.test.tsare test fixtures, so both are fine. - Nothing touches
finetune/or the export path.claim/checkAuthorsare new job_end fields, so please make sure they don't change the fine-tune Grok exclusion filter later. - The branch is
claude/, it has theclaudelabel, and it targets a stack branch, notmain. 👍
Must fix
- Advisory review mode loses "unconfirmed" and "unreviewed" on the terminal and in the window. With
reviewAdvisory,tierOfstill returnsverified(plus areviewNote) when the reviewer contradicted the task or didn't run. ThenclaimLabelsetsclaim, and the newev.outcome === "verified" && ev.claimbranch insrc/cli.tsx(around L1426) is checked before thereview && !review.confirmedandunreviewedbranches. So a run that used to printpassed its checks, unconfirmed/passed its checks, unreviewednow printsverified (independent checks: j)with no qualifier.ui/app.ts(around L1144) has the same ordering bug. Either move the claim branch below those two checks, or append, unconfirmed/, unreviewedto the claim whenreview.confirmed === falseorev.unreviewedis set. Please add a test: an advisory run with a judge-authored check and a2/3contradicting review should still say unconfirmed.
Should fix
- Behavior change needs docs. A headless
--criteria autorun without--judgenow exits 3 where it used to exit 0. That will break CI scripts and any bench arm that has no judge. Please add aCHANGELOG.mdentry and update the README "Terminal-Bench" paragraph (around L402–406: "the model drafts acceptance checks… it is held to them") to say that "verified" needs--judge <another model>or person-approved checks.bench/harbor/README.mdshould say the same, sincemolt_agent.pyruns--criteria auto. - Base / mergeability. The PR targets
claude/sync-local-work, so GitHub shows 8 commits / 45 files, it'sCONFLICTING, and no checks ran on the branch. Please retarget toclaude/overflow-classify(#30) so the diff is the one commit, or rebase once the stack lands, and get CI green before merge.
Nits / notes
sealDraftreadsprocess.env.MAAT_JUDGE_MODELdirectly instead ofenv("JUDGE_MODEL"), so a legacyMOLT_JUDGE_MODELwon't mark checks as judge-written.judge.tshas the same behavior, so this is consistent, but it's worth knowing.- Checks approved in the interactive interview (
app.tsx,taskChecksFromwith nohidden) are labelledverified (your checks)even though a model proposed them. That matches the PR's design. "approved by you" might read more accurately, but it's your call. normalizeModelIderrs toward "same model" (it strips everything before the last/, plus date and alias suffixes). That's the right direction because it fails closed. The tests coverdeepseek-v3vsv2andqwen3-235bvs32b, which is good.- The known gap where the reference is written by the worker even with a judge set is well documented. A follow-up issue to draft the reference with the judge model would close it.
The core rule is solid: unknown authorship defaults to the worker, ids are compared under every name the worker ran as, and person checks keep "verified". I'm happy to approve once #1 is fixed and #2 is documented.
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.
A run was labelled verified on checks the worker model drafted for itself. On the 2026-10-06/07 container bench, self-judged arms were right in 125 of 174 verified claims (72%) and separate-judge arms in 48 of 55 (87%); the wrong verifieds on the latest builds were all self-judged. Who wrote each task check is now recorded when it is sealed: the worker, a separate judge (--judge / MAAT_JUDGE_*), a person (done.yml, a mission contract, criteria approved in the window or given with --criterion), or the reference writer. A run is verified only when a passing check that ran the work and asserted a value came from someone other than the worker model, compared by provider-normalized id, or from a person. Otherwise the tier is passed-own-checks: outcome unverified, exit code 3. Claims read "verified (independent checks: <judge>)", "verified (your checks)" or "passed own checks (<worker>), not verified" on the terminal, the window, job_end's new claim field, the receipt index and the receipt. Receipts say who wrote each check. bench/local/run.py carries job_end's claim and checkAuthors; stats.py reads both the new and the old text.
fc5aa1d to
064049f
Compare
|
rebased onto claude/completion-reserve@a48570d, CI green |
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.
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 064049f. Range-diff against fc5aa1d, the commit my 21:20 UTC review looked at, shows only the rebase onto #36. The content is identical, so both items from that review are still open. I re-read the whole commit with a hostile worker in mind.
Checks run: typecheck is clean. The full unit suite passes 2040/2040 on Linux with node 20.19; the runner exits 1 only because node's coverage reporter crashes. pytest bench/local/tests: 26 passed, 1 skipped.
Pairing terms: clean. The only vendor names are the prefix regex and comment in normalizeModelId, which aren't product copy. No subscription backends, and fine-tune data is untouched.
Blocking
1. A passing check from the default maat init bar counts as "your checks", so self-judged runs still read "verified". src/tiers.ts:278 and 298. High confidence: reproduced with tierOf.
- The rule as coded:
person = passing.some((r) => authorOf(r).kind === "person"). A visible check with no recorded author is a person's. So any passing non-hidden result earnsbasis: "person", with no runs or value requirement. That includes the session builtins everymaat initbar ships (src/detect.ts:230):work-landed: files-changed,record-intact,claims-grounded,work-accounted,spec-intact. - Failure scenario: in any initialised project, run
maat run --criteria autoheadless with no--judge. The worker drafts every hidden check and changes a file, sowork-landedpasses. The run is tierverified, basisperson, labelled "verified (your checks)", exit 0.tierOf({results:[work-landed ok (builtin), task:out ok (hidden, value, author worker m)], worker:["m"]})returns{"tier":"verified","basis":"person"}. With only a surface check pluswork-landed, the result is the same. - Why it matters: this is the self-judging hole the PR exists to close. Your own description says a person's check must be one that "ran the work and asserted a value". The receipt also prints "written by: a person (your check)" for a builtin nobody wrote for this task.
- Fix:
personshould need a passing person-authored command check, at least excludingkind: "builtin"and session-tagged checks. Builtins shouldn't print as person-written.
2. The advisory-mode label still prints "verified" when the review contradicts the work or doesn't run. src/cli.tsx:1458 and ui/app.ts:1144. High confidence. Unchanged since my last review.
- What happens: with
--review-advisory,tierOfreturnsverifiedwith areviewNote. The newev.outcome === "verified" && ev.claimbranch comes before thereview.confirmed === falseandunreviewedbranches. So the terminal and the window print "verified (independent checks: j)" instead of "passed its checks, unconfirmed" or "unreviewed". - Fix: test
review.confirmed === falseandev.unreviewedbefore using the claim, or put them into the claim text.
3. The exit-code change is still undocumented. README and CHANGELOG. High confidence.
- What happens: headless
--criteria autowithout--judge, or with a judge that is the same model, now exits 3 instead of 0, and there's a newpassed-own-checkstier. Neither README nor CHANGELOG mentions either. - Why it matters: with the npm publish planned for tomorrow, CI jobs and bench scripts that treat exit 0 as success will flip without warning.
Non-blocking
normalizeModelIdmisses common aliases of the same model.src/tiers.ts:134. Medium-low confidence. A workerqwen/qwen3-235b-a22b-2507and a judgeQwen/Qwen3-235B-A22B-Instruct-2507normalise to different ids (…-instruct-2507), so a model ends up judging itself while counted as independent. The operator sets this, not the worker, so it's not exploitable from inside a run. Consider stripping-instruct/-chat/-it, or warning when the two ids share a stem.- Authorship is now the only gate, so a weak judge's checks still decide. Medium confidence; cross-reference to #34. A judge's check that prints a boolean and exits 0 is tagged runs+value and earns "verified (independent checks: …)" on wrong work. Example:
python3 -c "import csv; print(list(csv.reader(open('out.csv'))) == [['a'],['b']])". I reproduced it on #34's head, and #34 doesn't catch it either. - An empty worker id makes a real judge count as not independent. Low confidence; I didn't verify which backends hit this.
independentOfreturns false when there are no worker names, so ifcfg.modeland the reported model are both empty, a real judge never counts.
Nothing merged.
|
Grok bug-hunt (verdict correctness) — two findings in this PR's diff, both reproduced on the #34-head build. Not a full review (that pass is separate); these are from the hostile-worker verdict pass. 1.
|
…driver's own count The driver imported an import-style deliverable into its own process, so a module with os._exit(0) at top level ended the driver with exit 0 before any input was compared, and the reference check passed on wrong work. Each input now runs in a fresh child (runner.py) that loads check.py, drops reference() and INPUTS from the module, calls run_deliverable and sends repr() of the result back on its own pipe. A child that ends without a result (os._exit, sys.exit, a crash) is a result that matches nothing. Expected values stay in the driver's memory: never on disk, never in a child. The driver ends with "REFERENCE COMPARED n/n", and the bar requires that line for a pass: an exit 0 without it is a failure.
A passing check from the default `maat init` bar (work-landed, record-intact, claims-grounded, work-accounted, spec-intact) had no recorded author, so it counted as a person's check and turned a self-judged run into "verified (your checks)". Only a command check a person wrote or approved now carries the person basis; builtins and session-tagged checks never do, a result with no recorded kind fails closed, and receipts say these checks are Maat's, not "a person". `diff out.txt expected.txt` (any expect/golden/want/baseline/answer operand) counted as asserting a value even when the worker wrote both files. The seal now records each such operand's content before the first step, and at the tier a check whose value rests only on such a file counts only when the file predates the work and is unchanged (src/golden.ts). With --review-advisory, a review that contradicted the task or did not run no longer disappears behind the claim: claimLabel appends ", unconfirmed" or ", unreviewed", and the terminal and the window share one jobEndWords that tests both before using a claim. normalizeModelId also drops -instruct/-chat/-it, so a judge reached under another provider's spelling of the worker model is not counted independent. CHANGELOG, README (exit codes, Terminal-Bench) and bench/harbor/README.md document that headless --criteria auto without --judge now exits 3.
|
Pushed Round 2 (00:11 UTC)Blocking 1: default
Blocking 2: advisory label hid "unconfirmed" / "unreviewed". Fixed.
Blocking 3: exit-code change undocumented. Done.
Non-blocking
Round 1 (21:20 UTC)
Bug-hunt (verdict correctness)1. A worker-written golden file counted as a value assertion. Fixed in
2. The reference check passed when the deliverable called
Replay
|
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.
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.
Builds on #30 (claude/overflow-classify → #27 → #24–#26). Merge those first; this diff is one commit on top.
Why
A model was allowed to find the work and also judge it. On the 2026-10-06/07 container bench:
regex, where its checks shared its own misreading of the IP trap, and Qwen3-235B)Every one of those claims was labelled "verified (self-checked)", including the ones a separate judge checked.
What changes
author: { kind: worker | judge | person | reference, model? }. CLI--criteria autodrafts arejudgewhen--judge/MAAT_JUDGE_MODELis set, otherwiseworker. Criteria a person approved in the window, gave with--criterion, put in done.yml or a mission contract count asperson. A reference check isreferencewith the model that wrote it. If no author was recorded, a hidden check counts as the worker's.tierOf). A run is verified only when a passing check that ran the work and asserted a value was written by a person, or by a model other than the worker. Model ids are compared after provider normalization, soopenrouter/m:freecounts as the same model asm, and so does a dated alias. Both the configured worker id and the id the backend reported are compared. If all such checks are the worker's, the run gets the new tierpassed-own-checks, with outcomeunverifiedand the same exit code (3). A reviewer contradiction and an unreviewed claim still take priority, as before.verified (independent checks: <judge model>),verified (your checks),passed own checks (<worker model>), not verified. These appear on the terminal and in the window, in job_end's newclaimfield (pluscheckAuthors), in the receipt index row and in the receipt.written by:line per check in the Output section, a "written by" note on each sealed task criterion, and aClaim:line.run.py'sclaimfield now takes job_end'sclaim, falling back to the oldoutcome + " (self-checked)"for older builds.stats.pyaddsclaim_basis/claim_judge, which read both old and new text, andpassed-own-checksjoinsTIERS.summarize.pyhandles the new tier.startswith("verified")parsers keep working: the new verified labels still start with "verified", and the passed-own-checks label does not.Interactive and desktop sessions are unchanged, because their checks are person-approved.
Known gap, left as is: the reference check is still written by the worker model even when a judge is set, so a reference never counts as independent unless its writer is a different model.
Tests
test/independent-checks.test.ts: model-id normalization; worker-only checks → not verified; judge-written → verified; a judge with the same model id (including under a provider prefix) → not independent; person-approved → verified; the label text in each case; receipts and index rows record authorship and the claim.test/exit-codes.test.ts:--criteria autowithout a judge, or with a same-model judge, exits 3; with--judge jit exits 0.test/reference.test.ts: a reference written by the worker's own model is passed-own-checks.bench/local/tests/test_claim_labels.py:run.py'sclaim_ofon new and old job_end events, plusclaim_basis/claim_judge/is_verifiedon old and new text.npm run checkpasses with~/.local/binoff PATH: 1999 node tests, e2e PASS, self-check PASS.pytest bench/local/tests: 27 passed.