<plugin-root>names the directory that holds this plugin's.codex-plugin/plugin.json. Resolve it once from where this file was loaded, then substitute it into every path below that starts with it. Arguments:[<path-to-plan-or-spec> | <topic>] [--challenger=<profile>] [--rounds=N] [--dry-run] [--apply]. Wherever<arguments>appears below, substitute the text the user typed after the skill name. This plugin declares an MCP server,peer-review, started withuv run --script <plugin-root>/skills/cross-model-peer-review/scripts/server.py. This host does not start a plugin-declared MCP server on its own: register that command under that name in the host's MCP configuration before running this workflow, because its calls to that server fail until it is connected.
Cross-Model Peer Review
Orchestrates one deliberation run against protocol/PROTOCOL.md: builds an immutable
packet, gets explicit consent, sends it to an external challenger model, runs an
evidence-backed multi-round dialectic between the challenger and a repository-grounded
respondent, and computes a verdict from a ledger that is never hand-edited.
Critical rules
- Execute phases in order: 0, 1, 1b, 2, 2b, 3, 4, 5, 6, then
--apply. Do not skip or reorder them. - The consent gate (Phase 1b) is absolute. No call to the
peer_asktransport tool may be reachable before it. Callingpeer_profilesbefore the gate is fine: it is a local capability check, never network egress. --dry-runstops at the consent gate, before it asks for a decision.- The verdict (Phase 6) is computed from
03-ledger.mdonly. Prose in the verdict may explain a state; it must never assign or change one outside the ledger. - A run with findings never terminates after round 1.
--roundsbelow 2 is rejected per R11. - Never hardcode this plugin's install path. Self-references use
<plugin-root>/.... - Every
peer_askcall can return{"error": "..."}instead of a reply. Follow "Transport error handling" below at every call site; never treat an error payload as a challenger reply. - Never end a turn waiting. Wherever the run needs the operator, it asks with
AskUserQuestion; wherever it stops for good, it says the run is over and why. A turn that ends on a printed question is indistinguishable from a finished run, and an operator who reads it as finished waits for a run that is waiting for them. This is a dialogue between two people, not a prompt on a terminal: consent and every other answer are read for what they mean, never matched against a required word.
Files this run writes
All paths are relative to the run directory .peer-review/YYYY-MM-DD-HHMM-<slug>/.
| File | Written by | Phase |
|---|---|---|
00-brief.md |
peer-review:brief-builder, brief mode only |
0b |
00-packet.md |
peer-review:packet-builder |
1 |
01-challenge-r1.md |
command, from the challenger's transport reply | 2 |
01b-amendment.md |
command | 2b |
02-response.md |
peer-review:respondent |
3 |
03-ledger.md |
command, updated every phase, history appended per finding | 2, 3, 4, 5 |
05-challenge-r2.md, 07-challenge-r3.md |
command, from the challenger's transport reply | 4 |
06-response-r2.md, 08-response-r3.md |
peer-review:respondent |
4 |
09-certification.md |
command, from the challenger's transport reply | 5 |
10-corrective.md |
command (challenger section) plus peer-review:respondent (respondent section), only if a MISREPRESENTED flag is substantiated |
5 |
04-verdict.md |
command, computed from the ledger | 6 |
sent/r2.md, sent/r3.md, sent/certification.md, sent/corrective.md |
command, one file per outgoing payload after round 1 | 4, 5 |
Every outgoing payload is a file before it is a request. The transport reads the
payload off disk and takes no inline text (content_path, see mcp/server.py), so
each peer_ask call is preceded by the write that creates its payload file. Round 1's
payload is 00-packet.md itself and needs no copy; every later round writes its
payload under sent/ first. This is R15 made mechanical rather than requested: a
payload that must be retyped into a tool argument gets summarized instead, which is
exactly the run-invalidating defect R15 exists to prevent, and it is undetectable
downstream because every later check reads the file rather than the request.
Transport error handling
Applies to every peer_ask call in Phases 2, 4, 5, and the corrective round of Phase 5.
If the tool returns {"error": "<message>"} instead of a reply (text, usage,
model, latency_ms, sent_path, sent_bytes, sent_sha256):
- Do not write a challenge or response file from it, and do not touch the ledger for this call.
- Record the phase and the verbatim error message in a running "Transport failures" note kept in memory for Phase 6.
- Stop issuing further
peer_askcalls for the rest of the run. - If this happened on the very first call (Phase 2, round 1), there is no ledger yet
and nothing partial exists to protect. Report the verbatim error, then ask with
AskUserQuestionwhether to send the same packet again or end the run. A timeout is the common case here and it is transient: the challenger is thinking and the socket cannot tell that apart from a dead connection. Retrying resends00-packet.mdunchanged, under the consent already given at Phase 1b, so it needs no second gate; say so in the option text. Do not tell the operator to relaunch the command by hand, and do not attribute the failure to the gateway without evidence: the error message names what was hit and the profile field that changes it. On the second failure, report both and end the run. Do not write04-verdict.md. - Otherwise, skip straight to Phase 6. Every finding still
OPENorCHALLENGEDat that point is reported in its own "Interrupted by transport failure" verdict subsection, not folded intoSTANDOFF: an unanswered call is not the same claim as a saturated one.
Phase 0: Setup
This command has two modes, decided in step 1 and differing only in where the
artifact comes from. Artifact mode judges a plan or spec already on disk. Brief
mode materializes the session's context and decisions into 00-brief.md first, then
judges that. From Phase 1 onward the two are the same run: every later phase reads
<artifact path>, and Phase 0b is what sets it in brief mode.
- Parse
<arguments>. Flags:--challenger=<profile>(optional),--rounds=N(default3),--dry-run,--apply. The first non-flag token, if any, selects the mode:- Resolves to an existing readable file: artifact mode, that file is the artifact path.
- Looks like a path but does not exist (contains
/or\, or ends in.mdor.markdown): stop with a not-found error naming the path, and print the usage fromargument-hint. Never fall through to brief mode here: a mistyped path must not silently become a topic. - Anything else: brief mode, and the token is the topic hint that scopes which decisions the brief covers.
- No non-flag token at all: brief mode with no topic hint, covering the session's decisions as a whole.
- Validate
--rounds. Valid range is2to3: round 1 always runs in Phase 2, and the canonical file layout only names challenge/response files through round 3 (05-challenge-r2.md/06-response-r2.md,07-challenge-r3.md/08-response-r3.md). A value below2is rejected outright, citing R11 (a run with findings may never terminate after round 1). A value above3is clamped to3with a printed note, since no canonical file names exist beyond it. Default3. - Validate the artifact (artifact mode only; brief mode has no file yet and skips
this step). Read the file at the given path.
- Refuse if it does not exist, or is not readable as text.
- Refuse anything that looks like a unified diff (starts with
diff --git, or contains--- a//+++ b/header pairs) or a source file (extension outside.md/.markdown), with a message pointing at/senior-review:code-reviewfor that kind of target instead. - This command reviews intent artifacts, never code changes. The diff refusal holds in both modes: a brief describes decisions, and a request to challenge a diff is redirected the same way.
- Compute the run directory. Slug: in artifact mode, the artifact's basename
without extension; in brief mode, the topic hint, or
session-briefwhen there is none. Either way lowercased, with non-alphanumeric runs collapsed to a single hyphen and the result truncated to 48 characters. Timestamp: localYYYY-MM-DD-HHMM. Directory:.peer-review/<timestamp>-<slug>/. On a name collision (a concurrent invocation in the same minute), append-2,-3, ... Create the directory before any write. - Resolve the challenger profile. Call the
mcp__peer-review__peer_profilestool with no arguments. It returns{default, profiles: [{name, base_url, model, api_key_env, params, key_source, warnings, available}], source}.paramsis the profile's extra request fields (reasoning_effortand the like), already validated: a profile whoseparamsthe server refuses reportsavailable: falsewith the reason inwarnings.- Chosen name:
--challengervalue if given, elsedefault. - If
defaultis null and no--challengerwas given: stop. No profile is configured (peer_profiles'ssourcefield isnullwhen no profiles file was found at all). Point at<plugin-root>/skills/cross-model-peer-review/scripts/profiles.example.json, the.peer-review/profiles.json/~/.peer-review/profiles.jsonlocations it can be placed at, and thePEER_REVIEW_PROFILESenvironment variable, which names any other path to check first, ahead of both defaults. - If the chosen name does not appear in
profiles: stop, listing the configured profile names, thesourcepathpeer_profilesreported (which file was actually loaded), and pointing at<plugin-root>/skills/cross-model-peer-review/scripts/profiles.example.jsonandPEER_REVIEW_PROFILESfor placing or relocating a profiles file. - If the chosen profile's
availableisfalse: stop, quoting itswarnings. When the cause is the key, name itsapi_key_envvalue as the environment variable that must be set; when the cause is a refusedparamsfield, name the field. Point at<plugin-root>/skills/cross-model-peer-review/scripts/profiles.example.json. - Otherwise record the resolved profile's
name,base_url,modelandparamsfor later phases.
- Chosen name:
- Initialize an empty transport-failures list and an empty token-accounting list, both held in memory for Phase 6.
Phase 0b: Brief
Brief mode only. In artifact mode, skip this phase entirely and go to Phase 1.
This phase runs after profile resolution, not before it, so an unconfigured or unavailable profile stops the run before an agent is spent building a brief.
Spawn
peer-review:brief-builderwith the run directory and the topic hint:Task: subagent_type: "peer-review:brief-builder" description: "Materialize the decision brief" prompt: | Run directory: <run directory> Topic hint: <topic hint, or "none: cover the session's decisions as a whole"> Materialize this session's context and decisions into 00-brief.md per your protocol. Report back the brief's byte size, the count of taken decisions, the count of open decisions, and the "could not be sharpened" list verbatim.On return, confirm
00-brief.mdexists in the run directory. If it does not, report the agent's failure and stop; nothing was sent, nothing to clean up.Freeze. From this point the brief is the artifact, and
<artifact path>in every later phase means<run directory>/00-brief.md. It is never edited again for the rest of the run: that is what makes R2 hold for a materialized artifact, and what lets Phase 1's independent digest recheck compare three values that were never supposed to diverge. Wanting a different brief means a new run, not an edit.Vagueness stop (doctrine). If the brief carries no taken decision and no open decision that passed the builder's decidability self-check, stop here and say so. A packet built from that brief would produce findings standing on air, which is the one case the doctrine says not to spend a run on. Report the "could not be sharpened" list so the user can see exactly what was too vague.
Print the brief's path, its byte size, and the two decision counts, followed by the "could not be sharpened" list when it is non-empty. The full brief text is disclosed inside the packet at the Phase 1b consent gate, which is where a look before egress belongs; this line is a pointer, not a substitute for reading it.
Phase 1: Packet
Compose the mandate text. Use the template for the mode this run is in.
Artifact mode: "Judge whether
<artifact path>'s decisions, its plan of action, and each rejected alternative's rationale hold up under scrutiny. Prose style, formatting, and any file the artifact merely mentions without proposing a change to it are out of scope."Brief mode: "Judge whether the situation as described is the right framing of what is being decided, whether each taken decision's rationale holds, and whether each open decision's option set is complete. The taken decisions themselves are settled and are not to be relitigated: their reasoning is what is on trial. Prose style and formatting are out of scope."
Spawn
peer-review:packet-builderwith the artifact path, the run directory, and the mandate text:Task: subagent_type: "peer-review:packet-builder" description: "Build the challenge packet" prompt: | Artifact path: <artifact path> Run directory: <run directory> Mandate: <mandate text from step 1> Build 00-packet.md in the run directory per your protocol. Report back the byte size of 00-packet.md and its section list.On return, confirm
00-packet.mdexists in the run directory. If it does not, report the agent's failure and stop; nothing was sent, nothing to clean up.Independent digest recheck (R15). Before trusting anything the agent reported, verify three values agree, not two:
Recompute the artifact's own byte length and sha256 directly from the source file on disk:
wc -c <artifact path> python -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" <artifact path>Read the
bytes:andsha256:lines00-packet.mdrecords immediately above the embedded artifact: the packet's own claim about the source.Extract the embedded artifact block itself from
00-packet.md(the text between thosebytes:/sha256:lines and the start of the next section,## Ground truth), and hash and measure that extracted text the same way. This is the text the outgoing request will actually carry. Checking only the recorded digest line (step 2) proves nothing about whether the embedding below it was truncated or altered after that line was written; R15 requires source, packet embedding, and outgoing request to be byte-identical, and the embedding is what step 3 checks.
All three byte-length and sha256 values (source recompute, packet's recorded digest, packet's embedded-text digest) must match exactly. If any pair disagrees: abort the run before any transport call. Report which values disagreed and leave the run directory in place for inspection. This is the run-invalidating defect R15 exists to catch; a mismatch is never a warning.
Phase 1b: Consent gate
No transport call of any kind precedes this phase.
Digest the packet file. Phase 1 hashed the artifact and its embedding. What travels is the whole packet, so hash the packet file itself and carry the value as
<PACKET_SHA>for the rest of the run:python -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" <run directory>/00-packet.mdByte cap pre-flight. Compare
00-packet.md's byte size (<N>below) against the transport's payload cap, 400000 bytes (thepeer_asktool refuses anything larger outright, permcp/server.py). If<N>exceeds the cap, do not present the gate below: report the packet's size against the cap and stop. Approving a packet the transport will refuse is not a real consent decision; the artifact or the material it names must be reduced and the run repeated.Present verbatim, with the actual values substituted for the placeholders:
About to send this packet to an external service:
destination: <base_url> model: <model>
request params: <params, one `key=value` per entry, or `none`>
size: <N> bytes (transport cap: 400000 bytes)
sha256: <PACKET_SHA>
sections: Mandate, Artifact, Ground truth, Constraints, Considered and rejected,
Known weaknesses, Open questions, Out of scope, Response contract
Nothing else leaves this machine. Later rounds, certification, a corrective round
and any granted repository excerpt travel under this same consent.
Where <base_url>, <model> and <params> come from the profile resolved in Phase 0
(<params> is shown because those fields go out with every request), <N> is
the byte size of 00-packet.md, and <PACKET_SHA> is step 0's digest. The digest is
shown because consent is given to one specific file, and the transport reports back
the digest of what it actually sent: the two are compared in Phase 2, which is what
makes this gate a decision about a document rather than about an intention.
Ask, using
AskUserQuestion. Do not end the turn on printed text: to the operator, output that asks for nothing is output from a run that finished. HeaderSend packet, questionSend this packet to <model> at <base_url>?, two options:- Send it - "Transmits the -byte packet. Rounds, certification and granted excerpts follow under this consent."
- Do not send - "Nothing leaves the machine. The packet stays at /00-packet.md."
--dry-run: stop here, before the question. Report the packet path (<run directory>/00-packet.md) and that nothing was sent. End the command.- Consent is read as intent, never as a token. The operator answers a question;
they are not entering a password.
Send it,ok,yes,sì,vai,procedi,dai,go ahead,send, or any other plain affirmative in any language proceeds to Phase 2.no,stop,annulla,not nowor any plain refusal aborts: report that consent was withheld, leave the run directory in place, end the command. Never treat a clear yes as a refusal because of its wording. An affirmative carrying a condition or a question ("ok but what is in it?", "yes, without the appendix") is not consent yet: answer what was asked and ask again. - Ambiguity is asked about, not decided. If a reply is genuinely unclear, ask once more, naming both outcomes in one sentence. A second unclear reply ends the run with consent withheld. Silence is never consent, and neither is an unrelated instruction.
- The gate can be paused and resumed. An operator who answers something else entirely has not refused: answer them, then re-ask. The packet on disk is frozen and its digest is recorded, so the delay costs nothing and the gate is still about the same bytes.
Phase 2: Round 1
Load the Round 1 prompt: read
<plugin-root>/skills/cross-model-peer-review/references/round-prompts.mdand extract the fenced block under## Round 1 (critique).Call the
mcp__peer-review__peer_asktool:profileis the resolved profile name,systemis the Round 1 prompt text,content_pathis<run directory>/00-packet.md. Pass the path. The packet is never pasted into the call: the transport reads it, and that is the only reason R15's third link holds.Handle the error shape per "Transport error handling" above.
Verify what was sent (R15). Compare the reply's
sent_sha256against<PACKET_SHA>from Phase 1b. They agree unless the packet changed on disk between the consent gate and the call, which is a violation of the packet's immutability: on a mismatch, write no challenge file, touch no ledger, report both digests, and stop the run. Recordsent_bytesandsent_sha256for the verdict's transmission section.On success, write the
textfield verbatim to01-challenge-r1.md. Note theusage,model, andlatency_msfields in the token-accounting list under round 1.Initialize
03-ledger.md. Parse the## Findingssection of01-challenge-r1.md. For each findingF<NN>, create one entry using the template from<plugin-root>/skills/cross-model-peer-review/references/finding-lifecycle.md:Finding F<NN> claim (verbatim): <copied exactly from the reply> falsifier (verbatim): <copied exactly from the reply> | admissibility: (pending) challenger evidence: <the failure scenario / section attacked, as given> respondent position: (pending) respondent evidence: (pending) restatements: none state: OPEN new evidence since previous round: n/a (round 1) history: - R1: raised (severity <severity>)Transmission-artifact check (R15), applied before any finding is left OPEN. Key this on substance, not on the free-text
section attackedlabel the challenger wrote in its own words. For each finding, locate the specific material its claim and failure scenario actually reference (a quoted phrase, a named decision, a specific fact) inside00-packet.md: the embedded Artifact text, Ground truth, Constraints, Considered and rejected, Known weaknesses, Open questions, or Out of scope. Phase 1's digest check proved the packet byte-identical to the source, and step 4 proved the request byte-identical to the packet, so material that genuinely cannot be located anywhere in the packet cannot be a live claim about the artifact; it is noise the challenger introduced. Both halves are load-bearing: without step 4 this check would archive as challenger noise whatever the sending side had dropped, and its errors would all fall on the side of absolving the artifact.- Substance absent everywhere in the packet: set that entry's
statedirectly toTRANSMISSION_ARTIFACT, skippingOPEN, and record what was actually searched for and not found in place ofrespondent evidence. Do not send these findings to the respondent in Phase 3. - Substance present, but the
section attackedlabel names the wrong section or only paraphrases it: this is a labeling mismatch, not a transmission artifact. Leavestate = OPEN, add a softsection-label mismatchnote to the entry's history (naming the section the substance was actually found at), and send the finding to the respondent in Phase 3 as normal. The respondent has full repository access (R8) and judges the substance on its merits regardless of which section the challenger thought it lived in.
- Substance absent everywhere in the packet: set that entry's
Findings capped at 12 are the challenger prompt's responsibility, not this command's. If a reply carries more than 12, record every one of them anyway and note the overrun in the ledger's history for round 1.
Phase 2b: Context amendment
Parse the
## Context requestssection of01-challenge-r1.md. Each line names a locator.For each locator, in order, with running totals starting at 0 files / 0 bytes:
- Refuse if the locator resolves outside this repository (an external URL, an
absolute path outside the repo root, a
..traversal), or does not resolve to an existing file at all. Refuse reason: "outside the repository" or "not found". - Refuse if granting it would push the running totals past 10 files or 200 KB total. Refuse reason: "context amendment cap reached (10 files / 200 KB)".
- Refuse if the locator names material already present in
00-packet.md(the challenger already has it). Refuse reason: "already in the packet". - Otherwise grant it: read the file, add its byte size to the running total, increment the file count.
- Refuse if the locator resolves outside this repository (an external URL, an
absolute path outside the repo root, a
Write
01b-amendment.md:# Context Amendment, Round 1 ## Granted | Locator | Bytes | Note | |---|---|---| ## Refused | Locator | Reason | |---|---| Running totals: <N> files / 10, <K> KB / 200 KB.Granted material is not sent to the respondent (which already has full repository access per R8 and does not need packet-supplied facts). It is prepended to the round 2 challenger payload in Phase 4, each fact tagged
GIVEN, with its locator.
Phase 3: Response (round 1)
Spawn
peer-review:respondentwith the still-open findings (everything in03-ledger.mdnot alreadyTRANSMISSION_ARTIFACT) and the ledger itself:Task: subagent_type: "peer-review:respondent" description: "Answer round 1 findings" prompt: | Challenge file: <run directory>/01-challenge-r1.md Ledger: <run directory>/03-ledger.md Response file to write: <run directory>/02-response.md Answer every finding in the challenge file whose ledger state is OPEN. Findings already TRANSMISSION_ARTIFACT are closed; do not re-open them.On return, for each answered finding, update only the fields the respondent role owns (
falsifier admissibility,respondent position,respondent evidence,restatements,new evidence since previous round). Never let the agent's report changeclaim,falsifiertext, orstatedirectly; those stay under this command's control.Apply state transitions, per finding:
admissibility: INADMISSIBLE(the one restatement request already exhausted):state = UNTESTABLE, regardless of the respondent's position. This is the "falsifier fails admissibility twice" transition fromfinding-lifecycle.md.admissibility: RESTATED(pending the challenger's confirmation, not yet given): the finding is held open. Leavestate = OPEN; per R10, a restatement cannot support a verdict until the challenger confirms it, so no position-based transition applies yet. Carry it into Phase 4 for confirmation.admissibility: OK, positionACCEPT:state = RESOLVED_ACCEPT.admissibility: OK, positionREFUTE:state = CHALLENGED. The challenger has not yet seen this refutation; it is not resolved until certified.admissibility: OK, positionNEEDS-EVIDENCEorDISAGREE:state = CHALLENGED.
Append one history line per finding:
R1: respondent <position>, admissibility <outcome> -> <new state>.
Phase 4: Challenge rounds (2..N)
Runs for round 2, and round 3 if --rounds is 3. Each round processes only the
still-open set: findings whose state is OPEN or CHALLENGED after the
previous round, minus anything terminal (RESOLVED_*, STANDOFF, UNTESTABLE,
TRANSMISSION_ARTIFACT), minus any CHALLENGED finding flagged saturated: no further rounds by step 7 below. A saturated REFUTE finding is not terminal, but it
is also not resent: its outcome is already settled pending certification. Every step
below that says "still-open finding" or "still-open subset" means exactly this set;
this is where that exclusion is enforced.
For round r (2 or 3), files 05-challenge-r2.md/07-challenge-r3.md and
06-response-r2.md/08-response-r3.md:
- Load the Challenge round prompt: read
<plugin-root>/skills/cross-model-peer-review/references/round-prompts.md, extract the fenced block under## Challenge round (2..N). - Build the round payload: for each still-open finding, its
claimandfalsifierverbatim, the respondent's currentpositionandevidence, and (round 2 only) any material granted in Phase 2b, each fact taggedGIVENwith its locator. If a finding's ledger entry carries a pending proposed restatement (restatements: RESTATED AS "<wording>", not yet confirmed), include that wording too, labeled plainly as the respondent's proposed restatement awaiting the challenger's confirmation, so there is something for the challenger'sCONFIRM-RESTATEMENTorREJECT-RESTATEMENTreply to answer. - Write the payload, then send it. Write step 2's payload to
sent/r<r>.mdin the run directory, then callmcp__peer-review__peer_askwith this prompt assystemandcontent_pathpointing at that file. The file is not a copy of what is sent, it is what is sent: the transport accepts no inline text. Handle the error shape per "Transport error handling" above. - On success, write the reply to the round's challenge file. Note
usage,model,latency_msunder this round in the token-accounting list, and the reply'ssent_bytesandsent_sha256for the verdict's transmission section. - Per finding, apply the reply (
WITHDRAW,MAINTAIN,REFINE,CONFIRM-RESTATEMENT, orREJECT-RESTATEMENT):WITHDRAWnaming specific falsifying evidence:state = RESOLVED_WITHDRAWN. Append history:R<r>: withdrawn, evidence <cited>.WITHDRAWnaming no evidence: the finding does not close.statestaysCHALLENGED. Flag the entryunexplained withdrawal(a dedicated field or a history tag); the verdict reports this as a run weakness regardless of what eventually happens to the finding.MAINTAINwith new evidence or a new argument stated: recordnew evidence since previous round: YESon the challenger side,statestaysCHALLENGED.MAINTAINwith an explicit "no new evidence": recordnew evidence since previous round: NOon the challenger side.REFINE: recordrestatements: RESTATED AS "<the new wording>", pending. The original claim and falsifier are never overwritten (R10); the restatement travels alongside them.statestaysCHALLENGED; the restated wording is what the respondent answers in this round's response step, and confirmation of the restatement is implicit in the respondent's willingness to answer it (there is no separate confirmation step defined beyond the respondent's next reply).CONFIRM-RESTATEMENT, answering a proposed falsifier restatement shown in this round's payload (step 2): set the falsifier's admissibility toOKusing the restated wording, and recordrestatements: RESTATED AS "<wording>" confirmed by challenger in R<r>.statestaysOPEN; the finding rejoins the still-open set for this round's response step (step 6) so the respondent can investigate the now-admissible falsifier for the first time.REJECT-RESTATEMENT, answering the same proposal: the one restatement request R9 allows is now spent and refused. The original falsifier was already inadmissible as stated (that is why a restatement was proposed at all), sostate = UNTESTABLEnow.- If a
RESTATED(pending) admissibility finding was carried in from Phase 3 and this round's challenger reply does not useCONFIRM-RESTATEMENTorREJECT-RESTATEMENTfor it, it remainsOPENand is carried forward again; do not force a transition on an unconfirmed restatement.
- Spawn
peer-review:respondentfor the still-open subset, same shape as Phase 3, targeting this round's response file (06-response-r2.mdor08-response-r3.md). Handle its reply with the same transition table as Phase 3 step 3, plus: the respondent also reportsnew evidence since previous round: YES | NOfor its own side (did it change position or cite new material this round). - Saturation test, per still-open finding, per
finding-lifecycle.md: if the challenger side'snew evidence since previous roundisNOand the respondent side's is alsoNO, and neither side's position changed from the previous round, saturation has occurred. What happens next depends on the respondent's current position, mirroring step 8'sREFUTEbranch below rather than treating every saturated finding alike:- Position
REFUTE: do not setSTANDOFF. Leavestate = CHALLENGEDand addsaturated: no further roundsto the entry (a dedicated field or a history tag). This excludes the finding from every later round's payload (enforced by the still-open set definition in this phase's intro, which steps 2 and 6 both draw from) without finalizing it, so it falls through unresolved into Phase 5 step 1's existing collection clause, which already treats aCHALLENGEDfinding whose current position isREFUTEas a proposedRESOLVED_REFUTEand sends it to certification. Certification is the point where R12 actually applies: the challenger compares its own verbatim original words against the respondent's one-line closing rendering, a comparison a mid-debateMAINTAINreply never shows it, so saturating early does not substitute for that check. Append history:R<r>: saturation, both sides NO new evidence, positions unchanged, REFUTE carried to certification (not STANDOFF). - Position
NEEDS-EVIDENCE,DISAGREE, or no position recorded: setstate = STANDOFFimmediately (not cap-terminated: this is evidence saturation). No further rounds process this finding. Append history:R<r>: saturation, both sides NO new evidence, positions unchanged -> STANDOFF.
- Position
- Round cap. After the last round this invocation runs (round 3, or round 2 if
--roundswas clamped or given as 2), findings still open are resolved by their most recent respondent position, never swept as one block:CHALLENGEDwith the respondent's current positionREFUTE: not swept. It carries forward as a proposedRESOLVED_REFUTEinto Phase 5.finding-lifecycle.md's own transition table has a "proposed RESOLVED_REFUTE -> CHALLENGED" line, which presupposes exactly this proposed state as certification's input; sweeping it toSTANDOFFhere would make that transition, and certification itself, unreachable. Anyunexplained withdrawalflag on the finding still carries into the verdict regardless of this promotion.CHALLENGEDwith positionNEEDS-EVIDENCE,DISAGREE, or no respondent position recorded: becomesSTANDOFF, labeledcap-terminatedin its history line, distinct from the saturation label in step 7. Anyunexplained withdrawalflag still carries into the verdict.OPEN(aRESTATEDadmissibility outcome the challenger never confirmed): terminatesUNTESTABLE, notSTANDOFF. It never received a substantive respondent position and its falsifier was never admissible as stated (R9, R10);finding-lifecycle.mdroutes procedural failures toUNTESTABLE,TRANSMISSION_ARTIFACT, orCERTIFICATION_FAILED, never toSTANDOFF, and an unconfirmed restatement is exactly such a procedural failure, not a substantive contest that survived the evidence.
Phase 5: Certification
Collect every finding whose
stateisRESOLVED_ACCEPT,RESOLVED_WITHDRAWN,STANDOFF, orUNTESTABLE, plus everyCHALLENGEDfinding whose most recent respondent position wasREFUTE(see the note below on whyRESOLVED_REFUTEonly exists starting in this phase). For each, attach a one-line rendering of how it closed, in the respondent's own words where the respondent produced one.Note on
RESOLVED_REFUTE: nothing in Phases 2 to 4 ever assigns this state directly (aREFUTEposition only ever producesCHALLENGED, per R12 and R13's requirement that the challenger certify a refutation before it is final). Certification is the only place a finding can reachRESOLVED_REFUTE: aCHALLENGEDfinding whose most recent respondent position wasREFUTEand which the challenger certifies (does not flag, or flags unsubstantiated) in this phase becomesRESOLVED_REFUTEhere. Fold this promotion into step 3 below.TRANSMISSION_ARTIFACTfindings are excluded: they closed before the respondent ever answered them, so there is no respondent rendering to certify against, and no claim of the challenger's own words to defend.Load the Certification prompt: read
<plugin-root>/skills/cross-model-peer-review/references/round-prompts.mdunder## Certification. Write the collected findings plus renderings tosent/certification.md, then callmcp__peer-review__peer_askwith the prompt assystemandcontent_pathpointing at that file. Handle the error shape per "Transport error handling" above.On success, write the reply to
09-certification.md. For each finding:CERTIFIED: finalize its state. If the finding entered this phaseCHALLENGEDwith aREFUTErespondent position, setstate = RESOLVED_REFUTEnow. Anything already terminal stays as it was, now certified.MISREPRESENTED, substantiated (the quoted original words genuinely contradict the rendering, checked against the ledger's verbatimclaimfield, which never changed): this is a procedural failure of the refutation (R12), so it applies to findings the certification step was about to finalize asRESOLVED_REFUTE. Revertstate = CHALLENGED, strike any restatement (restatements: none), and continue to step 4.MISREPRESENTED, unsubstantiated (no real contradiction, or no quote given): discard the flag, note it in history, finalize the state asCERTIFIEDwould.MISREPRESENTED, substantiated, flagged against a finding that did not enter this phase asCHALLENGEDwith aREFUTEposition (aRESOLVED_WITHDRAWN,STANDOFF, orUNTESTABLEclosure): R12 ties this mechanism to a procedural failure of the refutation, and none of these closures was produced by the respondent's rendering, so there is nothing to invalidate.statedoes not change. Record the flag and the challenger's quote in09-certification.mdand carry it as a rendering dispute alongside that finding's normal reporting in Phase 6, distinct from a certified refutation's substantiated misrepresentation.
Corrective round, at most once for the whole run, only for findings reverted in step 3. If a corrective round has not already run in this invocation:
- Write only the reverted findings' original verbatim claims and falsifiers to
sent/corrective.md, then callmcp__peer-review__peer_askonce more withcontent_pathpointing at that file andsystembuilt from the same Challenge round prompt semantics but scoped to "answer against your original claim, this is a corrective round, not a debate" framing. Handle the error shape: on failure here, treat every reverted finding asCERTIFICATION_FAILED(no budget left to retry) and record the transport error as the reason instead of budget exhaustion. - On success, write the challenger's part under a
## Challengerheading in10-corrective.md. - Spawn
peer-review:respondenttargeting `10-corrective.md
- Write only the reverted findings' original verbatim claims and falsifiers to
…(truncated)