feat(launchpad): adjudicator.md -- what the judge is told (#118 STEP 5) - #265
feat(launchpad): adjudicator.md -- what the judge is told (#118 STEP 5)#265serina-mcfall wants to merge 3 commits into
Conversation
Normative sibling to ADJUDICATION.md, FINDINGS.md, and CONTAINMENT.md: states the adjudicator's scope, its four exclusions (no restating a claim as evidence, no hunting new defects, no approval-shaped output, no refuting for want of evidence -- UNPROVEN is the default), that anchor "pr" is a legitimate finding shape rather than a malformed one, and a worked restatement-vs-independent-check contrast. Names no model, per #117/#118's own framing. This step's done-when is textual on purpose, per the plan: whether the exclusion clauses work behaviorally is STEP 9's job (live recordings), not this one's, so no test/build command applies here. The .claude/.verified stamp for this commit was touched, not earned by a suite -- nothing in this worktree runs, imports, or lints this markdown file (verdicts.py and run_adjudication.py, which would consume it, live on sibling unmerged branches per this build's own scope: STEP 5 needs only STEP 1). Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… STEP 5) review-code found that adjudicator.md's citation of run_adjudication.py's _location_description behavior named an unmerged sibling branch without a commit SHA, unlike ADJUDICATION.md's own citation convention. This plan's own text has been burned three times by exactly this failure mode -- a true-when-written cross-branch claim going stale before merge -- so pin it to 2be90c6 (feat/review-agent-adjudication-nonce, PR #264, CI-green) rather than leaving it to drift silently if that branch is revised again before #118 merges. No test suite applies to this docs-only file -- .claude/.verified touched per verify-gate's own stated escape hatch for a project with no runnable suite, stated here rather than silently worked around. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Review pipeline — PR #265Stages run: Not applicable, declared rather than faked:
STEP 5's done-when, clause by clause
All six met. I checked each mechanically rather than reading for impression. Findings1. High — the document normatively directs the judge to write to
|
ciaran-slow
left a comment
There was a problem hiding this comment.
Comment review recording the pipeline result. Not an approval and not a change-request — the merge decision is unchanged by this.
Reviewed via the full pipeline — detail in my comment on this PR.
The document itself needs no change. I checked all six of STEP 5's done-when clauses mechanically and every one is met: four exclusions each with its reason, UNPROVEN stated in those words, a worked restatement-vs-independent-check contrast, anchor pr declared legitimate, and no model named anywhere (grepped for every vendor and family). The worked contrast at :41-59 is the strongest part — it distinguishes restatement from real evidence by a criterion that is actually operable: "whether the evidence could have been written by someone who never opened the file."
I also expected the commit citation to be stale and it is not. 2be90c629 exists, is an ancestor of #264, and _location_description at that commit does branch on anchor before touching file/line, exactly as claimed. Given the plan records getting this class of claim wrong three times and deriving a standing rule from it, that is worth naming.
Two holds, neither a defect in this diff: a dependency and a merge-order constraint.
-
:66is contingent on #263. It normatively instructs the judge to record new observations inadjudication.notes, and the runner provides no channel —notes=[]is hardcoded and the judge protocol has no such key. Probed: a judge returningnoteshas it silently dropped, leavingverdict_evidenceas its only outlet, which is the field the plan's OPEN item flags as unguarded. If #263 gains the wire, this line is correct as written and needs nothing. If the channel is deferred instead, this line needs to say so. Either way the resolution happens on #263, so this PR should not merge ahead of that decision. -
Merge after #264. This PR's base is
launchpad, andrun_adjudication.pydoes not exist there — I verified. The document cites it at:112-118and discloses that honestly, so it is not a hidden dangling reference, but landing ahead of #261→#263→#264 leaveslaunchpadcarrying a normative document citing a function no file onlaunchpaddefines.
…es deferral (#118 STEP 3) Two findings adjudicated on #263. **`_run_judge_safely` did not fail closed on blank evidence.** The guard was `verdict not in verdicts.VERDICTS or not evidence` -- a truthiness test, so `not " "` is False and whitespace passed as usable output. Reproduced through the shipped `--replay` flag, no code injection: a recording carrying `verdict_evidence: " \n "` published a CONFIRMED verdict at exit 0, and `verdicts.validate` returned zero violations because the contract check used the same idiom. A CONFIRMED Blocker is what blocks a merge, so this could publish a merge-blocking verdict with no stated reason. The rule is now `verdicts.is_nonempty_str`, imported rather than re-implemented. That matters more than the strip() itself: this producer guard and the contract check drifted apart precisely because each had its own copy, and each admitted whitespace because the other did. The docstring said "empty verdict_evidence", which was literally accurate -- `" "` is not empty -- so it has been corrected to say what the guard enforces. The promise it actually broke was the clause before it, "fail closed on anything unusable", and ADJUDICATION.md's own words it quotes. **`adjudication.notes` was hardcoded empty with no deferral stated.** Every other hardcoded-empty field is named in the module docstring's STEP 6/7 deferral list; `notes` was the one that was not, while ADJUDICATION.md declares it and `verdicts.py` carries it -- so a reader had every reason to assume the channel worked. Now documented, at the docstring and at the assignment. Deliberately NOT resolved here: `adjudicator.md` (#265) normatively tells a judge to "record it in `adjudication.notes`", against a protocol that drops the key. Plumbing `notes` means designing how notes are collected and attributed, which is a STEP 6/7 decision, and amending #265 is #265's call. The tension is now stated in the code so it cannot merge past unnoticed rather than silently picked. 6 tests added, each failing before the change. One of them originally passed for the WRONG reason and was fixed: the replay recording format is a mapping `finding_id -> {...}`, not a flat record, so the first version missed the lookup entirely and asserted UNPROVEN against "no recorded judge output". It now carries a control proving the lookup HITS and returns CONFIRMED on good evidence, so the UNPROVEN in the blank cases is the guard firing. 25 tests in test_run_adjudication (was 19), 176 across launchpad/review-agent. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Brings #264's `StagesShapeError` / `_input_stages` and #261/#263's `verdicts.is_nonempty_str` up to the chain tip. Both of this PR's findings were homed on earlier branches -- the `stages` Blocker on #264 and the `notes` drift on #263 -- so this branch is cleared by propagation rather than by any change of its own, which is what the adjudication asked for. Clean merge, no conflicts. 230 tests across launchpad/review-agent. `notes` remains empty and is now documented as deferred at STEP 6/7, with the unresolved tension against `adjudicator.md` (#265) stated in the code. #265 should not merge ahead of that decision. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…118 STEP 5) Prohibition 2 told the judge to "record it in `adjudication.notes`". That is a normative instruction against a channel that silently drops its input: `run_adjudication.py` hardcodes `notes=[]` and its judge protocol reads only `verdict`, `verdict_evidence`, `severity` and `severity_reason`. Probed on the STEP 3 branch -- a judge returning a `notes` key produces `adjudication.notes: []`, and the document validates clean against both contracts, so nothing catches the loss. `ADJUDICATION.md` declares the field and `verdicts.py` carries it, which is what made the instruction look supported. Nothing between the declaration and the producer connects them. Adjudicated across #263 and #267 as one cross-step drift. Resolved on this side rather than by plumbing `notes` through the protocol, because collecting and attributing notes -- per-finding or per-document, deduped, ordered -- is STEP 6/7's design, and building that surface here would pre-empt a decision those steps own. The deferral is now recorded from both sides: this document and `run_adjudication.py`'s module docstring. Also states the consequence the old wording left implicit: with the channel deferred, a new observation has nowhere to go from this stage, so `verdict_evidence` must not be stretched to carry it. That field is the reason for the verdict on the finding the judge was given, and it is the one field with no structural guard -- exactly the wrong place for overflow. Doc-only. 121 tests across launchpad/review-agent green on this branch. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
benmitchell11
left a comment
There was a problem hiding this comment.
Independent review — first pass on this one, no standing review to cross-check against.
Checked out feat/review-agent-adjudicator-md and ran the suite: Ran 121 tests ... OK, matching the PR body's own claim of "confirms nothing regressed" (this is a docs-only change, correctly not claiming to prove anything about the diff itself).
Stacking note, for the record rather than as a problem: this PR's base is launchpad directly, not feat/review-agent-adjudication-nonce (STEP 4) like the review-in-step-order assignment would suggest. The PR body states this is deliberate ("Depends only on STEP 1 ... so it branches directly off launchpad rather than stacking on the in-flight STEPs 2-4") and that holds up — it's a pure markdown addition with no code dependency on STEPs 2-4. Worth flagging only because it means this PR does not carry #264's nonce/stages fixes or #263's whitespace-evidence fix, so a reader diffing branches shouldn't expect them here.
Content review:
- The four prohibitions are each given a stated reason, and prohibition 1's worked contrast (restatement vs. independent check) is genuinely useful — it's concrete enough that a reader could apply the test to a real judge transcript.
- Prohibition 3 ("no approval-shaped output") is honest about being only partially mechanical: the doc itself says
_FORBIDDEN_KEYSlives inverdicts.validate, which nothing calls automatically on the runner's own output before printing, so "no such key can exist" is stated as not proven, only "the runner writes only known keys, so it cannot produce a forbidden one today." That's the right amount of honesty for a spec document and matches what I saw readingrun_adjudication.pydirectly on the other branches —adjudicate()/main()indeed never callverdicts.validateon their own output. - The
notesamendment (commit05a960478) is real and matches what it claims: the original text told the judge to "record it inadjudication.notes", and this PR corrects that to say the channel is deferred and not to useverdict_evidenceas a substitute dumping ground either. I confirmed independently (readingrun_adjudication.pyonfeat/review-agent-adjudication-run/-escalate/-dedupe) thatnotes=[]is hardcoded and the judge protocol has nonoteskey — so this document's corrected claim is accurate. Worth noting for the author: #267's standing review flagged this exact cross-step drift and suggested "resolution belongs on #263" — this PR resolves it from the adjudicator.md side instead, which is arguably the more natural place (it's the instruction that was wrong), but the two PRs' resolutions should be cross-referenced so a reviewer of #267 doesn't think it's still open. - The
anchor: "pr"section's citation torun_adjudication.pypins a specific commit (2be90c629) on the still-unmergedfeat/review-agent-adjudication-noncebranch rather than citing the branch alone, explicitly to avoid citation rot before that branch merges — I confirmed that commit exists on that branch's history and does contain the_location_descriptionfunction as described (anchor branched on first,prcase never falls through to aNone:Noneformat string).
No blockers. Clean.
Not an independent review — I wrote part of this PRFlagging a conflict rather than posting a verdict, because a review is worth nothing if the reviewer wrote the thing. Commit So I cannot assess this PR's central document. I would be marking my own work, and the one thing that made the earlier adjudication of this stack worth anything was that a fresh context checked what the authoring context had claimed. What I can say without judging the content:
What it needs: a reviewer who did not write The specific question a real reviewer should press on: the deferral I wrote resolves the contradiction by telling the judge not to rely on the channel, rather than by making the channel work. That was @serina-mcfall's explicit decision between three options, and it is defensible — but it means a judge that genuinely notices a new defect has nowhere to put it until STEP 6/7, and the document now says so plainly. Whether that is acceptable, or whether |
Summary
Implements STEP 5 of
launchpad/plans/2026-08-13-issue-118-adjudication.md:adjudicator.md, the document stating what the judge is told. Depends only on STEP 1 (ADJUDICATION.md, merged via #256), so it branches directly offlaunchpadrather than stacking on the in-flight STEPs 2-4. Docs-only.Related issue
Refs #118
Issue type
Task
Agent provenance
Objective
Add
launchpad/review-agent/adjudicator.md— a prompt-definition file, sibling toADJUDICATION.md/FINDINGS.md/CONTAINMENT.mdand in the same normative voice.Impacted components
launchpad/review-agent/adjudicator.mdApproach and rejected alternatives
States the judge's scope and four prohibitions: do not restate a claim as its own evidence, do not hunt for new defects, do not emit approval-shaped output, and do not refute for want of evidence (UNPROVEN is the fail-closed default). Includes a worked contrast between a restatement and an independent check, because the distinction is the one a reader is most likely to get wrong, and states that anchor
pr(file and line both null) is a legitimate finding shape rather than malformed.Rejected: building a behavioural test that the prohibitions hold under a live model. Whether an exclusion clause survives contact with a real adjudicator is a property of output, which needs STEP 9's recorded before/after pairs — and tagging this step against STEP 9 would make the dependency circular. This step's own plan text says its done-when is deliberately textual, and no harness was expected here.
Rejected on the
notesprohibition: leaving it as originally written. See Escalations — it mandated a channel the runner discards.Verification
Command run:
Raw output:
Docs-only, so that run confirms nothing regressed rather than proving anything about this diff — nothing in it touches
launchpad/review-agent/*.py.The
notesclaim this PR now corrects, probed on the STEP 3 branch where the runner lives — a judge returning anoteskey alongside a valid verdict:The value is discarded and both contract checks pass, so nothing catches the loss.
Not verified
Nothing here is verified against a live model, by design. Every claim in this document is about what a judge is instructed to do; whether an instruction changes model behaviour is unmeasured and unmeasurable until STEP 9 exists. Treat this as a specification, not as evidence of behaviour.
The worked restatement-vs-independent-check contrast is illustrative, not sampled. It was written to make the distinction legible, not drawn from real adjudicator output — so it may not resemble the failure mode a real judge actually exhibits.
The four prohibitions have no mechanical enforcement. Prohibition 3 (no approval-shaped output) is the only one with any:
verdicts.py's_FORBIDDEN_KEYSwalk. And that walk lives inverdicts.validate, which neitheradjudicate()normain()calls on its own output before printing — so it is a validator a downstream caller may run, not an enforcement in the producer. The conclusion still holds (the runner writes only known keys, so it cannot produce a forbidden one), but "no such key can exist" would be stronger than the code supports. Prohibitions 1, 2 and 4 are textual only.Security implications
Adds one markdown file; no change to exposure. What it records is security-relevant: prohibition 3 is the document half of "escalate, never approve", which is the property that keeps an agent from approving work — forbidden by
launchpad/AGENTS.mdrule 1. Prohibition 4 makes UNPROVEN the default, so a judge that cannot establish something escalates rather than clearing it.The
notesamendment closes a small integrity gap rather than opening one. Instructing a judge to write into a channel that silently discards its input meant a genuine new observation had exactly one remaining outlet —verdict_evidence, the field with no structural guard. The amendment says so, and says not to stretch that field instead.Escalations
adjudication.notes".ADJUDICATION.mddeclares that field andverdicts.pycarries it, which made the instruction look supported — butrun_adjudication.pyhardcodesnotes=[]and its judge protocol never reads the key. Adjudicated across feat(launchpad): run_adjudication.py -- the adjudication CLI (#118 STEP 3) #263 and feat(launchpad): dedupe via an injectable second judge (#118 STEP 7) #267 as one cross-step drift. Resolved on this side rather than by plumbingnotesthrough the protocol, because collecting and attributing notes is STEP 6/7's design and building it here would pre-empt a decision those steps own. Decision taken by @serina-mcfall.notes, not to relax prohibition 2.run_adjudication.pycitation is pinned to2be90c629rather than to a branch name, after a Low finding on the citation-rot risk. That SHA is on feat(launchpad): nonce check and stages manifest (#118 STEP 4) #264, which is not merged — so the citation will need re-pinning if feat(launchpad): nonce check and stages manifest (#118 STEP 4) #264's history is rewritten.🤖 Drafted by Claude Code (
claude-opus-5) for @serina-mcfall.