TfP Docs

Authoring Review Protocols

A review protocol is the recipe TfP follows to get a judgment on a result when code alone cannot settle it. It says who may review, what they are shown, what they are asked, and how their answers turn into a decision. Once written, it runs the same way every time: the same reviewer pool, the same rubric, the same rule for combining answers. A Problem can use a protocol written by someone else, and the same protocol can be reused across many Problems.

How a protocol fits into verification

Every Problem says how its results will be checked. That description is a short list of checks, run in order — for example: first an automated check, then a review. Each item on the list is one of three things: a program TfP runs, an outside system TfP asks, or a review.

A review item names a review protocol. When a Submission gets that far, TfP runs the protocol: it opens review seats that eligible reviewers claim, collects their answers, and applies the protocol's rule to reach one of three results. Accept means the Submission passed this check. Reject means it failed. Unresolved means the reviewers could not settle it, and the Submission is marked as undetermined rather than failed.

flowchart LR
    VC[Verifier contract<br/>ordered checks] -->|review stage| RP[Review protocol]
    RP -->|run for one Submission| RPR[Review process<br/><i>seats, claims, answers, decisions</i>]
    RPR --> R[accept · reject · unresolved]

Each time a protocol runs, TfP keeps a record of that run: who claimed each seat, what each reviewer answered, what assistance they declared, and how the answers were combined. The same protocols are also used on their own, without a Submission being verified, to triage or rank candidates and to audit earlier decisions.

Who does the reviewing

TfP never picks a reviewer. When a review node opens, it opens a fixed number of seats. Anyone who is eligible under the node's rule and not conflicted can see the open seats — over MCP with pick_review, on the /reviews page, or through the API — and claim one. A claim holds the seat for a short time; the reviewer reads the materials with whatever tools they have, forms a judgment, and submits. A claim that is not completed in time lapses and the seat reopens.

Reviewers bring their own compute. A reviewer may work by hand, or with a coding agent, another model, or any tool, on their own account; the User who claimed the seat is the accountable reviewer either way, and whatever assisted them is recorded as a declaration on the record. Your protocol can require that declaration and restrict which agents are acceptable, but TfP does not run the reviewer's agent and does not verify the declaration.

There is one exception, covered at the end of this page: a node can ask TfP itself to obtain the judgments from a hosted model. That is an advanced option, not the normal path, and no production protocol uses it.

What a protocol is made of

A protocol is a small graph with two kinds of node.

A review node asks one or more reviewers for judgment. It says how many seats to open, who is eligible to claim them, whether reviewers work independently, what they can see, and what rubric they answer. Every reviewer returns a structured response plus, when the rubric asks for them, a rationale, citations, and a confidence.

A reduce node turns answers into a decision with one deterministic rule — count how many said pass, take the median score, pick the first decided outcome — and emits accept, reject, or unresolved. It never calls a model.

Nodes connect in three ways:

One reduce node is the protocol's output. There are no loops, no expressions, no user code, no timers, and no calls out to anything.

The smallest useful protocol

One accountable reviewer decides. This is the protocol behind the live findings-extraction Problem: one seat, open to the Campaign's named reviewers; the reviewer reads the exact bound source and the submitted document, with whatever agent they choose, and answers pass, fail, or uncertain. Pass accepts, fail rejects, and uncertain leaves the result unresolved.

flowchart LR
    S[Submission] --> R["reviewer<br/>1 seat · campaign reviewers"]
    R --> D["decision<br/>pass → accept · fail → reject"]
    D --> O[accept / reject / unresolved]

The same thing written down:

version: tfp.review/v0.1
purpose: acceptance
title: "Extraction fidelity: one accountable campaign reviewer"
subject_kind: submission

nodes:
  - id: reviewer
    type: review
    title: Campaign reviewer
    reviewers:
      kind: human
      count: 1
      independent: true
      eligibility: { rule: campaign_reviewers }
    visibility:
      prior_nodes: []
      stage_evidence: true
      hidden: [submitter_identity, submitter_organization]
    conflicts:
      exclude_submitter: true
      exclude_submitter_organization: false
    skills:
      - skill_version_id: skv_01jn5p2c9fxk
        requirement: required
    rubric:
      instructions_md: >
        Judge whether the submitted document is a faithful representation
        of the exact bound source. You are not judging whether the paper
        is right. You may use any agent or tool; you are the accountable
        reviewer, and if an agent assisted you, record it as assistance.
        Answer pass only if every material statement is supported by the
        source; fail if any is contradicted; uncertain if the source does
        not settle it. Cite the supporting passage for each finding.
      response_schema:
        type: object
        required: [verdict, material_errors]
        properties:
          verdict: { enum: [pass, fail, uncertain] }
          material_errors:
            type: array
            items:
              type: object
              required: [field, problem, source_locator]
              properties:
                field:          { type: string }
                problem:        { type: string }
                source_locator: { type: string }
      rationale: required
      citations: { min_items: 1 }
      confidence: required

  - id: decision
    type: reduce
    title: Reviewer decides
    inputs: [reviewer]
    op: k_of_n
    config:
      field: verdict
      accept: { predicate: { op: eq, value: pass }, k: 1 }
      reject: { predicate: { op: eq, value: fail }, k: 1 }

output: decision
policy:
  assignment_timeout_hours: 72
  claim_hold_minutes: 120

What happens when it runs. The Submission's automated check passes and the run parks on the review stage; the reviewer node opens one seat with a 72-hour deadline. A Campaign reviewer sees the seat in pick_review (or on /reviews), claims it, and holds it for 120 minutes. The claim returns the rubric and every material by reference: the submitted document, the bound source files, and the required Skill's SKILL.md, each with a hash and a Resource URI. The reviewer — or the agent working for them — fetches and reads those, answers the rubric, cites the passages by the same URIs, and, if an agent helped, declares it. A pass verifies the Submission; a fail refutes it; uncertain leaves the run undetermined. If the hold lapses before a response, the seat reopens and any eligible reviewer, including the same one, may claim it again. If nobody completes the seat within 72 hours, the process ends inconclusive with the reason recorded.

Declared assistance is optional here (assistance: {required: false, allowed: []} — the same as leaving it out): the rubric asks for it, and TfP records whatever is declared. The reviewers section below shows how to require it. This protocol, as JSON with every optional field written out, is the canonical example schemas/examples/review-protocol.campaign-reviewer.json, validated in CI against the schema.

The rest of this page is about what else a review node and a reduce node can say.

Review nodes

A review node answers three questions.

Who may review. How many seats, and who is eligible to claim them — anyone, members of one Organization, the Campaign's designated reviewers, or named people. count is how many reviewers must complete the node, and independent keeps them from seeing one another's answers; both are part of what the protocol means. A node may also require reviewers to declare the agent that assisted them and restrict which are acceptable.

What they see. Reviewers always see the Submission and its files. You choose whether they also see earlier reviewers' reports, evidence from earlier automated checks, and any Skills you want them to apply, and what is hidden from them — the submitter's identity, for example. Conflict rules keep the submitter and, if you choose, their Organization out of the eligible pool.

What they answer. A rubric: instructions in prose, plus the exact shape of the answer as a JSON Schema — a verdict, a score, a recommendation, or a whole structured report. Separately from that answer, every reviewer can be required to give a rationale, citations into the source material, and a confidence, so those read the same way across every protocol.

Reduce nodes

A reduce node turns answers into accept, reject, or unresolved with one of three rules:

Rule What it does Use it for
k_of_n Counts how many answers match a value; accepts or rejects at separate thresholds, otherwise unresolved Votes: one reviewer decides, majority, unanimity, two-of-three-else-escalate
threshold Aggregates a numeric answer (mean, median, min, max) and compares it to accept and reject cut-offs Scored rubrics
coalesce Takes the first decided outcome from a list of reducers Joining an escalation chain into one result

A reducer is deterministic and never calls a model. If combining answers needs judgment — three specialists' reports synthesized into one — that is another review node, and a reducer reads its answer.

Conditional branches

A node can carry when, naming a reduce node and one of its three outcomes. The node runs only if that reducer produced that outcome; otherwise it is skipped. That is the only conditional: escalation, triage, and "expert only on disagreement" are all built from it, and a coalesce at the end picks whichever branch decided.

Common shapes

Shape Built from
Single reviewer one review node → k_of_n with a threshold of 1
Independent panel, k of n one independent review node → k_of_n
Panel with a meta-reviewer independent panel → one reviewer who can read the panel's reports → k_of_n on the meta-reviewer
Sequential escalation review → reduce → when: unresolved review → reduce → … → coalesce
Triage then panel cheap review → reduce → when: accept panel → reduce → coalesce
Sampled audit any of the above, run over a sample of already-accepted results, with purpose: audit

These are available as templates that expand to ordinary protocol documents; a template has no separate behaviour.

A worked example

An escalation protocol. Three Campaign reviewers, each bringing an agent, independently apply an extraction-accuracy rubric. A reducer accepts if two say pass and rejects if two say fail. If neither side reaches two — a split, or an uncertain — one domain expert from a named list reads the Submission and the panel's reports and decides. A final reducer takes the panel's outcome when it resolved, otherwise the expert's.

flowchart LR
    S[Submission] --> A[Reviewer A]
    S --> B[Reviewer B]
    S --> C[Reviewer C]
    A --> PD["panel_decision<br/>k_of_n: 2 pass / 2 fail"]
    B --> PD
    C --> PD
    PD -->|unresolved| E[Domain expert<br/>reads panel reports]
    E --> ED["expert_decision<br/>k_of_n: 1"]
    PD --> F["final<br/>coalesce"]
    ED --> F
    F --> D[accept / reject / unresolved]

Five nodes: two review nodes (panel, expert) and three reduce nodes (panel_decision, expert_decision, final). The expert branch is conditional on panel_decision being unresolved. Written out in full:

version: tfp.review/v0.1
purpose: acceptance
title: "Extraction accuracy: three campaign reviewers, expert on disagreement"
subject_kind: submission

nodes:
  - id: panel
    type: review
    title: Independent reviewer panel
    reviewers:
      kind: human
      count: 3
      independent: true
      eligibility: { rule: campaign_reviewers }
      assistance:
        required: true
        allowed: []                 # any agent, but it must be declared
    visibility:
      prior_nodes: []
      stage_evidence: true
      hidden: [submitter_identity]
    rubric:
      instructions_md: >
        Assess whether the extracted reaction record faithfully represents
        the cited source. Answer `pass` only if every field is supported by
        the source; `fail` if any field contradicts it; `uncertain` if the
        source does not settle it. Cite the supporting passage for each
        judgment. Declare the agent that assisted you.
      response_schema:
        type: object
        required: [verdict]
        properties:
          verdict: { enum: [pass, fail, uncertain] }
      rationale: required
      citations: { min_items: 1 }
      confidence: required

  - id: panel_decision
    type: reduce
    title: Two of three
    inputs: [panel]
    op: k_of_n
    config:
      field: verdict
      accept: { predicate: { op: eq, value: pass }, k: 2 }
      reject: { predicate: { op: eq, value: fail }, k: 2 }

  - id: expert
    type: review
    title: Domain expert
    when: { node: panel_decision, outcome: unresolved }
    reviewers:
      kind: human
      count: 1
      independent: false
      eligibility: { rule: named_users, user_ids: [usr_01jq7expert01] }
    visibility:
      prior_nodes: [panel]
      stage_evidence: true
      hidden: [submitter_identity]
    conflicts:
      exclude_submitter: true
      exclude_submitter_organization: true
    rubric:
      instructions_md: >
        The panel did not agree. Read their reports, examine the source
        yourself, and decide whether the record faithfully represents it.
      response_schema:
        type: object
        required: [verdict]
        properties:
          verdict: { enum: [pass, fail] }
      rationale: required
      citations: { min_items: 1 }
      confidence: required

  - id: expert_decision
    type: reduce
    title: Expert decides
    when: { node: panel_decision, outcome: unresolved }
    inputs: [expert]
    op: k_of_n
    config:
      field: verdict
      accept: { predicate: { op: eq, value: pass }, k: 1 }
      reject: { predicate: { op: eq, value: fail }, k: 1 }

  - id: final
    type: reduce
    title: Final
    inputs: [panel_decision, expert_decision]
    op: coalesce
    config: {}

output: final
policy:
  assignment_timeout_hours: 72
  claim_hold_minutes: 120

Node by node:

Size count against the pool. It counts reviewers who complete, not seats opened: if the Campaign has four named reviewers and one of them is the submitter, a three-seat panel has exactly three people who can fill it, and the process ends inconclusive at the deadline if any of them does not.

Field reference

Everything in the language, in the order it appears in a document.

Element map

Every field in the language, before the detail:

Field / block Applies to Purpose
version protocol Exactly tfp.review/v0.1
purpose protocol acceptance / triage / ranking / audit — what the review means
title protocol Display name
subject_kind protocol What one process examines: submission, problem_proposal, campaign_proposal
nodes protocol The graph, 1–16 nodes
output protocol The reduce node whose outcome is the result
policy protocol The node deadline and the claim hold
id node Unique name, ^[a-z][a-z0-9_]{0,39}$
type node review or reduce
title node Optional label
when node Run only if a named reducer emitted a named outcome
reviewers review Kind, count, independence, eligibility and declared-assistance rule (human nodes) or allowed hosted models (agent nodes)
visibility review Prior nodes visible, stage evidence, what is hidden
conflicts review Exclusions by relationship to the subject (humans; optional)
skills review SkillVersions supplied as context
rubric review Instructions, response schema, and universal-field requirements
inputs reduce Upstream nodes read
op reduce k_of_n / threshold / coalesce
config reduce The operation's parameters

Document structure

version: tfp.review/v0.1
purpose: acceptance | triage | ranking | audit
title: string
subject_kind: submission | problem_proposal | campaign_proposal
nodes: [ ... ]
output: <id of a reduce node>
policy:
  assignment_timeout_hours: 1–720
  claim_hold_minutes: 15–1440 (default 120; shorter than the deadline)

purpose says what the review means. A review stage of a VerifierContract uses acceptance; triage, ranking, and audit describe reviews run for other reasons through the same machinery and do not change how the protocol executes. subject_kind is what one process examines; v0 protocols review one subject at a time.

The review node

- id: panel
  type: review
  title: optional label
  when: null | { node: <reduce id>, outcome: accept | reject | unresolved }
  reviewers: { ... }
  visibility: { ... }
  conflicts: null | { ... }          # humans; null = defaults
  skills: null | [ { skill_version_id: skv_..., requirement: required | recommended } ]
  rubric: { ... }

Reviewers

reviewers:
  kind: human                       # the default; see the hosted-agent section for `agent`
  count: 1–25                       # distinct reviewers who must complete
  independent: true | false
  eligibility:
    rule: any_participant | organization_members | campaign_reviewers | named_users
    organization_id: org_...        # with organization_members; the Organization's id
                                    # as shown in its URL, e.g. org_01jf90aa4rrc
    user_ids: [usr_...]             # with named_users
  assistance:                       # optional: declared-assistance rule
    required: true | false
    allowed: [ { provider: string, model: string }, ... ]   # empty = any

count is part of the protocol's meaning: "three independent reviewers, two must agree" is a different protocol from "one reviewer". The protocol fixes how many judgments are required; who supplies them is whoever eligible claims the seats. It counts reviewers who complete, not seats that were claimed, so size it against the pool: a node needing five members of an Organization with six eligible members, with the submitter excluded, has one spare.

independent: true puts each reviewer in their own independence group: none sees another's response before completing. Use it whenever you will count or aggregate responses.

Restricting to one Organization — only members of Organization X may claim:

reviewers:
  kind: human
  count: 2
  independent: true
  eligibility:
    rule: organization_members
    organization_id: org_01jf90aa4rrc

campaign_reviewers opens seats to the Campaign's designated reviewers; named_users to exactly those Users; any_participant to any authenticated User not excluded by conflicts. No reviewer-group object exists; eligibility is written here.

Declared assistance. A reviewer who uses an agent while forming their judgment is still the reviewer; the assistance is recorded on the assignment as provenance — {provider, model, config?}, with config an optional free-form description of how the agent was run. There is no human_with_agent kind. A node may require that declaration and restrict it:

reviewers:
  kind: human
  count: 1
  independent: true
  eligibility: { rule: campaign_reviewers }
  assistance:
    required: true
    allowed:
      - { provider: anthropic, model: claude-opus-5 }

TfP refuses a response on such a node unless it declares assistance whose provider and model match an entry in the list (any entry, when the list is empty), and records exactly what was declared. The entries are whatever strings you choose to name — TfP does not run the reviewer's agent, so it neither needs to recognise the provider nor verifies the declaration; the accountable User stands behind it.

Visibility and blinding

visibility:
  prior_nodes: [panel]              # earlier nodes whose responses this reviewer may read
  stage_evidence: true              # read-only evidence from earlier Contract stages
  hidden: [submitter_identity]

Reviewers always see the subject itself — the Submission and its Artifacts. Everything else is opt-in. prior_nodes is how a meta-reviewer reads a panel's reports; naming a node there makes this node wait for it. hidden withholds:

hidden value Withheld
submitter_identity The Attempt owner's handle and profile
submitter_organization The Organization the Attempt is attributed to
prior_scores The structured responses of nodes in prior_nodes; their rationale and citations stay visible
attempt_notes The Attempt's Notes

Conflicts

conflicts:
  exclude_submitter: true
  exclude_submitter_organization: false

Applies to human reviewers. Omit it (null) for the defaults shown; on a hosted-agent node it has no effect and is normally omitted. Check it against eligibility: excluding the submitter's Organization on a node whose reviewers must be members of that same Organization leaves nobody who can claim, and the process finishes inconclusive at the deadline.

Skills as context

skills:
  - skill_version_id: skv_01jn5p2c9fxk
    requirement: required

A technoeconomic-analysis review can require the exact TEA methodology Skill, so every reviewer applies the same method. A required Skill's SKILL.md is listed among the reviewer's materials, by reference, and each assignment records which SkillVersions were supplied.

Rubric

rubric:
  instructions_md: >
    What to assess, how to decide, what to cite.
  response_schema:                  # JSON Schema, draft 2020-12, describing an object
    type: object
    required: [verdict]
    properties:
      verdict: { enum: [pass, fail, uncertain] }
  rationale: required | optional
  citations: { min_items: 0–50 }
  confidence: required | optional

Every completed assignment returns four things. The response is the domain-specific judgment and is validated against response_schema. The other three are universal, live on the assignment rather than inside the response, and read the same across every protocol:

Universal field What it is Rubric requirement
rationale_md Communicable justification rationale: required or optional
citations Structured evidence pointers: the material's resource_uri as listed, plus locator and/or quote citations: { min_items: n }
confidence Overall confidence in [0, 1] confidence: required or optional

Do not declare rationale_md, citations, or confidence inside response_schema; validation rejects it. A response that fails the schema, or omits a required universal field, is refused with the problems named and nothing is recorded; the reviewer's claim stays open so they can fix it and resubmit within the hold.

The instructions should tell reviewers what to read and what to judge, and refer to materials by the key the bundle lists them under; they should not restate a material's hash, size, or provenance — the bindings hold those facts and the bundle shows them.

Large instructions belong in a Skill

instructions_md answers one question: what judgment are you making here? Keep it short and task-specific. Detailed methodology, a reviewer handbook, worked examples and edge-case guidance belong in SkillVersions pinned on the node's skills, which reviewers receive among their materials. response_schema stays inline because it is executable: the validator and the reducers read it. There is no linked-instructions abstraction; a long rubric is a sign that a Skill is missing.

Responses can be rich. A technology-analysis review might declare:

response_schema:
  type: object
  required: [feasibility, economics, recommendation]
  properties:
    feasibility:     { enum: [low, medium, high] }
    market:          { type: string, maxLength: 4000 }
    economics:
      type: object
      required: [capex_musd, lcoe_usd_mwh, key_assumptions]
      properties:
        capex_musd:      { type: number }
        lcoe_usd_mwh:    { type: number }
        key_assumptions: { type: array, items: { type: string } }
    sensitivities:   { type: array, items: { type: string } }
    risks:           { type: array, items: { type: string } }
    recommendation:  { enum: [pursue, watch, drop] }

A reducer would read recommendation; the rest is the reviewer's report, preserved on the assignment. Reviewers are never asked for, and TfP never stores, hidden chain-of-thought — rationale is the communicable justification.

The reduce node

- id: decision
  type: reduce
  title: optional
  when: null | { node, outcome }
  inputs: [panel]                   # one review node for k_of_n / threshold; reduce nodes for coalesce
  op: k_of_n | threshold | coalesce
  config: { ... }

A reducer never calls a model. It reads completed responses, applies its operation, and emits an outcome plus a recorded intermediate value.

k_of_n

Counts responses whose field is a given value or in a given set, with separate accept and reject thresholds.

op: k_of_n
config:
  field: verdict
  accept: { predicate: { op: eq, value: pass }, k: 2 }
  reject: { predicate: { op: eq, value: fail }, k: 2 }

Outcome: accept if at least accept.k responses match accept.predicate; otherwise reject if at least reject.k match reject.predicate; otherwise unresolved. Predicates are eq (a scalar) or in (a set) only, the accept and reject values must be disjoint, and accept.k + reject.k must exceed the reviewer count, so both outcomes can never hold. Numeric comparison belongs to threshold.

Want Config (count = n)
One reviewer decides accept.k: 1, reject.k: 1
Majority of 5 accept.k: 3, reject.k: 3
Unanimity, escalate on any split accept.k: n, reject.k: n
Two of three agree, otherwise escalate accept.k: 2, reject.k: 2
Accept on any pursue, reject only if all drop accept: {predicate: {op: in, value: [pursue]}, k: 1}, reject: {predicate: {op: eq, value: drop}, k: n}

threshold

Aggregates a numeric field and compares it to cut-offs.

op: threshold
config:
  field: score
  aggregate: mean                   # mean | median | min | max
  accept_gte: 4.0
  reject_lt: 2.5

Outcome: accept if the aggregate ≥ accept_gte; reject if < reject_lt; otherwise unresolved. reject_lt must not exceed accept_gte. Use min for "every reviewer must score at least X". The field may be declared number or integer; mind the boundaries with integer scores — on a 1–5 scale, reject_lt: 2 rejects only a median of exactly 1, so write reject_lt: 3 to reject medians of 2 and below.

coalesce

Takes reduce nodes in priority order and emits the first outcome that is not unresolved. Skipped inputs count as unresolved; if none resolved, unresolved.

op: coalesce
inputs: [panel_decision, expert_decision]
config: {}

This is how an escalation protocol produces one final result.

Why only three

The catalogue was derived from the protocols people actually asked for. k_of_n with two thresholds already expresses single-reviewer, majority, unanimity, and escalate-on-disagreement, so any, all, unanimous, and map would be aliases. threshold is what scored rubrics need. coalesce is the only way to combine an escalation chain. Weighted votes and confidence tie-breaks were left out until a real protocol needs them; adding an operation later invalidates no existing protocol.

Conditional continuation

when: { node: panel_decision, outcome: unresolved }

The only conditional in the language. A node with when runs if the named reduce node emitted the named outcome, and is skipped otherwise. Put when on both the review node and the reducer that reads it, as in the worked example, so the whole branch is conditional. when.node must not depend on the node carrying the condition.

Policy

policy:
  assignment_timeout_hours: 72      # the node deadline, from the moment its seats open
  claim_hold_minutes: 120           # how long one claim holds a seat; default 120, min 15

Reviews are claimed, not assigned. When a review node opens, its seats are visible to every eligible reviewer (pick_review), and the first to claim a seat holds it for claim_hold_minutes; a claim that is not completed in time lapses and the seat reopens to everyone eligible, including the reviewer whose hold lapsed. The node as a whole must be completed within assignment_timeout_hours of opening, or the process finishes inconclusive with the reason recorded. The hold must be shorter than the deadline. Keep holds short: they are what stops anyone sitting on a seat. There is no replacement budget — a reopened seat is just a seat — and a process never waits forever; this is the only place that behaviour is configured. (max_replacements_per_node from earlier documents is accepted and ignored.)

Templates as macros

The shapes listed under Common shapes are available as templates — single, parallel_k_of_n, panel_with_meta_review, sequential_escalation, triage_then_panel — that expand to ordinary protocol documents. They validate exactly like hand-written protocols and have no separate execution semantics. Sampled audit and ranking are uses of the audit and ranking purposes, not templates.

Static validation

Every protocol is fully validated before it can be pinned or run. The complete list is in the Review reference. The rules authors hit most:

Invalid examples

Each of these fails validation.

Reducer field not in the schema.

rubric:
  response_schema:
    type: object
    required: [verdict]
    properties: { verdict: { enum: [pass, fail] } }
# ...
config: { field: score, aggregate: mean, accept_gte: 4, reject_lt: 2 }   # `score` does not exist

Overlapping accept and reject values.

config:
  field: verdict
  accept: { predicate: { op: in, value: [pass, uncertain] }, k: 2 }
  reject: { predicate: { op: in, value: [fail, uncertain] }, k: 2 }      # `uncertain` on both sides

Both thresholds can hold.

reviewers: { kind: human, count: 3, ... }
config:
  accept: { predicate: { op: eq, value: pass }, k: 1 }
  reject: { predicate: { op: eq, value: fail }, k: 1 }   # 1 + 1 ≤ 3

Numeric comparison in k_of_n.

config:
  field: score
  accept: { predicate: { op: gte, value: 4 }, k: 2 }    # k_of_n takes eq / in only; use threshold

Universal field inside the response schema.

response_schema:
  type: object
  required: [verdict, confidence]                       # confidence is a universal field

Hold longer than the deadline.

policy:
  assignment_timeout_hours: 1
  claim_hold_minutes: 120                               # 120 min ≥ 1 h

Cycle.

- id: a
  type: review
  visibility: { prior_nodes: [b], ... }
- id: b
  type: review
  visibility: { prior_nodes: [a], ... }

Output is a review node.

output: panel        # must be a reduce node

Conditional on a review node.

when: { node: panel, outcome: accept }    # `when.node` must be a reduce node

Anything the language does not have — an expression in when, a node type other than review or reduce, an op outside the catalogue, a URL to call — is rejected by the schema's closed enums and additionalProperties: false.

Pinning a protocol from a Contract

# In a VerifierContract's stages
- key: faithfulness_review
  kind: review
  title: Source faithfulness
  description_md: Reviewers judge whether the record represents the source.
  review_protocol_id: rpt_01jq6p3c0gxk

Reviewers on nodes with stage_evidence: true can read what earlier stages produced. Changing which protocol a stage pins changes the Contract's acceptance semantics, so it is a new Contract and a new Problem Revision. Campaign defaults do not reach into existing revisions.

Advanced: TfP-hosted agent reviewer

Everything above has people claiming seats and bringing their own agents. A review node can instead be kind: agent, which asks TfP itself to obtain the judgments from a hosted model:

reviewers:
  kind: agent
  count: 3
  independent: true
  allowed:                          # exact configurations TfP may run, used round-robin
    - provider: anthropic
      model: claude-opus-5
      config: { effort: high, max_tokens: 16000 }

This is a different arrangement from the rest of the page, and the differences matter:

This path is not how participants bring agents to review — for that, use a human node and, if you want the agent named, an assistance rule. No production protocol needs the hosted path today; it exists for authors who specifically want TfP to run a fixed panel. Its coverage example is schemas/examples/review-protocol.escalate.json — a hosted-agent panel with config: {effort: high} for its first node and a human expert for its second, validated in CI against the schema. It is there to exercise the hosted path, not as the default shape; the default is the one-reviewer protocol at the top of this page.

What is recorded

Every process snapshots the exact protocol document and its hash. Every assignment records who judged (the accountable User who claimed the seat, or for a hosted node the exact agent configuration that ran), any agent assistance a reviewer declared, the Skills and prior reports supplied, what was hidden, the validated response, rationale, citations, confidence, and timestamps — including claims that were declined or lapsed, and, on a hosted node, an invalid reply with which requirement it failed. Every reducer step records its operation, config, exact inputs, and output. A decision is reconstructible from the process record alone, which is what makes review protocols comparable over time.