Skip to content

Post-work audit: checks drafted after the work, gated on the pre-work tree and on mutants - #42

Open
solvyxtech wants to merge 2 commits into
claude/discriminating-verifiedfrom
claude/post-work-audit
Open

solvyxtech wants to merge 2 commits into
claude/discriminating-verifiedfrom
claude/post-work-audit

Conversation

@solvyxtech

Copy link
Copy Markdown
Owner

Opt-in --post-work-audit (MAAT_POST_WORK_AUDIT=1). Builds on #34.

What it does

When a claimed turn ends unverified (the sealed checks passed without earning "verified", only drafted checks refused it, or nothing checked it), the --judge model drafts more checks. It must be a different model from the worker (#32). Runs that ended "not proven", errors, and runs stopped by the clock are left alone.

  • Interface view (src/interface-view.ts). The judge sees the task text plus:

    • which files were added or changed;
    • declaration lines only;
    • each new script's --help, kept only if it reads as usage text and run on a throwaway copy;
    • data files by name and format only;
    • the top-level listing.

    It never sees the transcript, the claim or the outputs. A subprocess judge starts in an empty folder.

  • Grounding. Each check quotes the task words its expected value comes from. A check whose quote isn't in the task is dropped (quotedIn).

  • Gates (src/post-audit.ts). A check must clear all of them:

    • (d) lints: L15 (mutates the work), L16 cannotFail (includes Redraft checks that pass before the work; discrimination gate opt-in #34's boolean-print class), and it must assert a value;
    • (a) passes on a copy of the work;
    • (b) fails on the pre-work copy. This reuses the seal-time screen, now exported as preWorkScreen, and respects intact();
    • (c) fails on at least one of 3–5 mutants of the changed code that still parses and didn't just crash. Mutations: flip a comparison, nudge a number, delete a statement, swap if/else. Data files are mutated only when no code changed.
  • Isolation. Every run is on a throwaway copy, and its process group is killed on exit (killGroupOnExit). Without that, a server left by one run would answer for the next.

  • Label. One check through every gate gives:

    • outcome verified, tier verified-audit;
    • claim verified (post-work audit: <judge>) in job_end, the receipt index and the journal;
    • job_end audit: {judge, drafted, grounded, accepted}.

    run.py/stats.py parse it (basis audit). Commands are withheld while they run, then released and journalled.

Replay (bench/local/audit_replay.py)

All 178 "unverified" trees from 2026-10-07's lanes, each replayed in its own bench container on this build. The task's setup() rebuilds the pre-work tree, and the judge is asked live.

judge arm grader-passed verified (recall) grader-failed verified
qwen3-coder-30b-a3b (bench judge) judged 6/90 (7%) 0/9
qwen3-coder-30b-a3b self (what-if) 7/65 (11%) 0/9
qwen3-coder-next judged 18/91 (20%) 0/10
qwen3-coder-next self (what-if) 15/66 (23%) 0/9

On the judged lanes, the pre-work "verified" was right 27/31 times. With the audit it is right 33/37 (a3b judge) or 45/49 (coder-next).

Most drafted checks don't get as far as the mutants. They assert nothing, or they fail on correct work. The zero false verifieds come from only 18–19 failing runs per judge, so the upper bound on precision is loose.

Tests: test/post-work-audit.test.ts (view excludes outputs/claims; ungrounded dropped; passes-every-mutant rejected; full-gate check earns the label in a turn; flag off changes nothing). npm run check green (2094) with ~/.local/bin off PATH.

Tyler Skelton added 2 commits October 7, 2026 18:11
… tree and on mutants

--post-work-audit (MAAT_POST_WORK_AUDIT=1), off by default. When a claimed
turn ends unverified (the sealed checks passed without earning the word,
only drafted checks refused it, or nothing checked it), the --judge model,
which must be another model than the worker, drafts checks from the task
text and an interface view of the work (src/interface-view.ts): the files
added or changed, their declaration lines, what each new script's --help
prints when it reads as usage, data files by name and format only, and the
top-level listing. It never gets the transcript, the claim or the outputs,
and a subprocess judge is started in an empty folder.

Each check must quote the task words its expected value comes from; a quote
not in the task (dispute.ts quotedIn) drops it. A check then has to clear
every gate (src/post-audit.ts): the lints (L15 mutates, L16 cannot fail, and
it must assert a value), pass on a copy of the work, fail on the pre-work
copy (criteria.ts preWorkScreen, the seal-time screen, now exported), and
fail on at least one of 3-5 mechanical mutants of the changed code that
still parses and did not just crash (flip a comparison, nudge a number,
delete a statement, swap if/else bodies; data files are mutated only when
the work changed no code). Runs leave nothing behind: every run is on a
throwaway copy and its process group is killed when it exits
(run.ts killGroupOnExit), so a server one run started cannot answer for
the next.

One check through every gate makes the outcome "verified" with tier
verified-audit and claim "verified (post-work audit: <judge>)" in job_end,
the receipt index and the journal (post-work-audit note with each check's
gates and mutant kills; the commands are withheld while they run and
released after). job_end carries `audit`. run.py keeps it; stats.py reads
the label as basis "audit".
…-07's runs

Replays the audit on every saved tree from today's lanes whose claim was
"unverified" (178 runs: 101 in judged arms, 77 in self arms as a what-if,
since no judge was set there), each in its own bench container with the
build installed: the saved tree is the work, the task's setup() rebuilds the
pre-work tree, the judge is asked live. Cached per run.

Judge qwen/qwen3-coder-30b-a3b-instruct (the bench's judge model):
  judged arms: 6 of 90 grader-passed runs verified (7%), 0 of 9 failed;
  self arms:   7 of 65 (11%), 0 of 9.
Judge qwen/qwen3-coder-next:
  judged arms: 18 of 91 (20%), 0 of 10;
  self arms:   15 of 66 (23%), 0 of 9.
Pre-work "verified" on the judged lanes was 27/31 right; with the audit,
33/37 (a3b judge) or 45/49 (coder-next).

Most drafted checks never reach the mutants: they assert nothing (a3b: 95
of 201 gated) or fail on correct work (80). Zero false verifieds is over
only 18-19 failing runs per judge, so the bound on precision is loose.
@solvyxtech solvyxtech added the claude Owned by claude label Oct 8, 2026

@solvyxtech solvyxtech left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grok review of #42: changes requested (posted as a comment because the PR and the reviewer share the solvyxtech account, so GitHub won't accept a formal Request changes. Not merging.)

I checked this out locally at af5f680. npm run typecheck is clean and the full suite passes, 2094 of 2094, including test/post-work-audit.test.ts with 11 of 11 green. CI never actually ran: the typecheck-and-test job wasn't started because of a GitHub Actions billing or spending-limit problem on the account, so the red X isn't a code failure.

On the pairing rules this PR is fine. There are no competitor names in the added lines, no subscription backends, and nothing feeds fine-tune data. Base is claude/discriminating-verified and not main.

Blocking

B1. The audit can overrule a failing independent sealed check and still award "verified" (src/engine.ts:4661-4664 together with src/engine.ts:4813-4822). Confidence: high.
checksDisagree turns "not proven" into "unverified" whenever every failing check is hidden. Judge-drafted sealed checks are hidden too (criteria.ts:154), so this covers more than the worker's own drafts. auditable then accepts that "unverified" outcome, and one accepted audit check sets the outcome to verified.
Failure scenario: the --judge model seals [ "$(python3 tool.py 3 4)" = "7" ] before the work. The worker's tool prints 8, so the check fails twice, checksDisagree fires, and the outcome becomes unverified. Next, the audit judge sees only the interface. It writes a check on a different property (say the output format), which passes on the work, fails pre-work because the file is missing, and kills a mutant. The run ends as verified (post-work audit) while an independent check is still failing on the same work.
Why it matters: this is a false "verified" built from a real failure signal, the exact case #32 to #34 were meant to close. The replay's 0 out of 9 or 10 grader-failed runs is too small a sample to rule it out. No test covers it.
Fix: skip the audit when any failing check was authored by someone other than the worker (checkAuthors already records this), or require the failing check to be retired first through the dispute arbiter. Please add a test where a failing judge-authored check plus a passing audit check still ends unverified.

B2. --help passes the work's actual output to the judge whenever it contains the word "usage" (src/interface-view.ts:148-150 and :230-232). Confidence: high, reproduced.
looksLikeUsage only checks /\busage\b/i. A script that ignores --help and just runs prints its real answer. If that answer contains "usage" (disk, CPU, memory or API usage reports are common tasks), the whole answer goes into the view. I reproduced it against dist/interface-view.js: a script that prints disk usage report and total: 1234 ends up in the view under "--help prints:" with total: 1234 verbatim.
Failure scenario: the judge copies 1234 as the expected value and quotes any 8 or more task characters for grounding (see N2). The check passes on the work, fails pre-work and kills a nudge mutant, so a wrong answer gets verified-audit. A hostile worker can also get this on purpose by printing "usage:" alongside its answer, or by putting instructions to the judge in --help text, add_argument(help=...) lines or usage: echo lines. Signatures keep those whole (interface-view.ts:128, :142).
Why it matters: the view promises "never its answers", and that promise is what keeps the audit honest.
Fix: require real usage shape (a usage: line at the start of a line, or argparse or getopt structure), reject help output that differs from what the script prints with no arguments at all, and strip default= and help= values from the signature lines. Add an adversarial fixture for an output that contains "usage".

Non-blocking

N1. Compile errors in compiled languages count as killed mutants (src/post-audit.ts:363-374 and :452). Confidence: medium.
mutantParses treats any extension without a parser (.go, .ts, .rs, .java) as parsing. CRASH only matches Python and JS error names. In Go, deleting a statement often gives declared and not used, so go run exits 1 and the run counts as killed by any check that runs the program. That makes gate (c) close to free there. The same thing happens with Node TypeError and with Python tracebacks over 2000 characters, where the header gets cut off at :472. Fix: also count a mutant run that fails while the baseline passed and stderr looks like a compiler error as crashedOnly, or run go vet, tsc --noEmit or a similar check as the parse step when it's available.

N2. Grounding doesn't connect the quote to the expected value (src/post-audit.ts:124). Confidence: high, low severity on its own. Any task span of 8 or more characters passes quotedIn, so a guessed or leaked value with an unrelated quote still counts as "grounded". Requiring the expected literal, or the input-to-output pair, to show up in or near the quote would make grounded mean what the job_end field says.

N3. The audit can overrun the run's deadline (src/engine.ts:3830, src/post-audit.ts:527). Confidence: medium. deadlineAt is at least now + 45s even when the turn's deadline has already passed, and it's only checked between checks, not between mutant runs. Worst case is about 4 checks × 7 runs × 10s, plus the judge ask. On a bench with a hard wall clock this could kill the container mid-audit. Please clamp to the turn deadline and check it inside the mutant loop.

N4. amendTier now raises a tier, but it was built and documented to lower one (src/engine.ts:4832, src/receipts.ts:776-783). Confidence: medium. The hash-bound receipt file still reads unverified, and only the mutable index row says verified-audit. Lowering in the index fails safe. Raising there means a "verified" with nothing hash-bound behind it, apart from the journal note. Either chain-bind the audit record, for example a small receipt with the accepted check hashes, or update the docs and the receipt-verify path so they treat an index-only raise as needing the journal entry.

N5. A subprocess judge in an empty folder isn't sandboxed (src/post-audit.ts:586-587). Confidence: low. An agentic ACP judge still has file tools and could find the work's outputs by path. Worth noting in the docs, or keeping the audit to API judges for now.

N6. Replay caveat. The replay trees are "the state after grading", so grader artifacts may be in the work tree the judge's interface view lists. Worth confirming they're excluded before the recall numbers get quoted.

The overall design is good: running cheap gates first, mutating only the changed lines, killing process groups and keeping a separate tier are the right calls. B1 and B2 are the two places where a wrong "verified" can still get through.

@solvyxtech solvyxtech left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: CHANGES REQUESTED

Prior items: 0 of 2 blockers fixed. B1 (the audit can overrule a failing independent sealed check) and B2 (--help output containing "usage" leaks real answers to the judge) are both still open, and so are N1–N6. The head is still af5f680, the commit I reviewed in 5450349722, and the base hasn't moved.

Fresh pass from Grok, comment only. I re-ran it at af5f680: npm run typecheck is clean, and the suite passed 2094 of 2094. CI hasn't run because of the account-wide GitHub Actions billing block. Two new, non-blocking notes:

1. Model vendor names were added to the repo. Non-blocking until Tyler rules; high confidence that they're there. bench/local/audit_replay.py hardcodes qwen/qwen3-coder-30b-a3b-instruct as the default judge, plus qwen3-coder-30b-a3b in an assert/fixture, and the commit messages name qwen/qwen3-coder-30b-a3b-instruct and qwen/qwen3-coder-next. My last review called this PR clean. That ID is an open-weight model the bench self-hosts, and it already appears in 24 files on main. Under Tyler's "no competitor names anywhere in the repo" rule, though, it's a judgment call, so I'm flagging it for him rather than deciding it. If it has to go, read the default from REPLAY_JUDGE_MODEL or the bench's untracked local config and use a generic fixture ID. Changing the commit messages would mean rewriting history, which is Tyler's call.

2. This PR and #43 conflict. High confidence. Both branch from claude/discriminating-verified, and a trial merge of claude/false-drops into claude/post-work-audit conflicts in src/criteria.ts. Whichever one lands second needs a rebase, and so does #44, which sits on this branch.

@solvyxtech

Copy link
Copy Markdown
Owner Author

Round-3 review, item by item. Fixed in claude/integration-all@3d1ca16 (#49); this branch is superseded by integration.

  • B1 (the audit overruling a failing independent check): fixed. The audit does not run if any failing sealed check was written by someone other than the worker (a judge, a reference, a person), using sealedAuthors + independentOf. Tests in test/post-work-audit.test.ts: a failing judge-authored check plus a passing audit check still ends unverified and the audit is never asked. A failing check that only the worker drafted still lets the audit run.
  • B2 (--help passing the answer to the judge): fixed. looksLikeUsage now needs a line that starts with usage:, or an argparse/getopt heading at the start of a line. --help output is used only when it differs from what the script prints with no arguments. A usage: line in the source is kept only up to the end of its string. add_argument default=/help=/metavar=/const= values are cut. The interface-view test now includes a "disk usage report" script and one that prints usage: next to its answer; neither answer reaches the view.
  • N1–N6: not done in this pass. N3 (deadline clamp) and N4 (an index-only raise) are still open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Owned by claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant