Prepare for a reviewer adapter
Status: blocked. This SDK publishes no reviewer stage.
There is no reviewer adapter type, no reviewer protocol, no stage schema and no client-tier tool that carries a verdict. If you came here for "plug your model into the gate", that does not exist yet, and this page will not pretend otherwise by inventing a schema and calling it the contract.
What you can do today is build and test the input/output contract your application owns, entirely offline, so the day a reviewer stage ships you are adapting working code instead of starting.
Prove the gap yourself
Do not take this page's word for it. The starter probes the installed package:
$ python examples/starters/reviewer.py --probe
{
"supported": false,
"candidate_detected": false,
"python_symbols": [],
"client_tier_tools": [],
"probed_symbols": ["doublegate_sdk.reviewer", "doublegate_sdk.review", ...],
"probed_tools": ["doublegate.review", "doublegate.judge", "doublegate.verdict", "doublegate.admit"],
"available_client_tools": ["doublegate.ban", "doublegate.hide", "doublegate.pending",
"doublegate.rank", "doublegate.recall", "doublegate.remember",
"doublegate.status", "doublegate.tip", "doublegate.why"],
"diagnostic": "reviewer_stage_unavailable: …"
}
The probe reports candidate names for inspection under candidate_detected.
They do not prove a compatible reviewer contract, and they do not change the
verdict: supported is a constant false in this build. It is not feature
detection — there is no reviewer code path here for a matching name to reach,
so any other value would claim an effect that cannot happen. Enabling a
reviewer stage takes an implemented, tested binding, never a newly matching
symbol.
--probe alone always exits 0; it is a report, not an assertion. To make the
gap fail a build rather than merely report itself:
What the starter does instead
examples/starters/reviewer.py
exercises one offline reviewer exchange using only machinery the SDK actually
publishes:
| Concern | SDK machinery used |
|---|---|
| Is the reviewer input well-formed? | schema.check_response against a caller-owned schema |
| Is the evidence in it real? | authoring.evaluate_file over a fixture |
| Is the content identity derived correctly? | submission.envelope_content_hash → content_digest_from_content_hash |
| Is the verdict well-formed? | check_response plus the SDK's closed vocabularies |
| Are the verdict's enumerated values legal? | reasons.normalise_category, vocabulary.normalise_blockers, skipped.normalise_skipped_reason |
$ python examples/starters/reviewer.py
{
"reviewer_stage": "unavailable",
"contract_owner": "caller",
"live_model_invoked": false,
"gate_contacted": false,
"admission_effect": "none",
"derived_input": {
"artifact_type": "memory",
"content_digest": "sha256:17b6ea2b…",
"deterministic_outcome": "flagged",
"human_review": "required",
"excerpt_policy": "withheld",
"findings": [{"kind": "required-text", "severity": "warning",
"detail": "required_text_missing: check 1",
"start": 0, "end": 0, "excerpt": ""}]
},
"derived_input_violations": [],
"fixture_input_violations": [],
"fixture_output_violations": [],
"vocabulary_violations": [],
"valid": true
}
Those four flags are the point. No model ran, no gate was contacted, and nothing was admitted.
Read this before you reuse the schemas
examples/starters/data/reviewer-input.schema.json and
reviewer-output.schema.json are caller-owned fixtures, not a product
contract. Both say so in their own description field. They are shaped so
that the parts which can be anchored are anchored:
categoryreuses the five valuesdoublegate_sdk.reasonscloses.blockersreuses the nine valuesdoublegate_sdk.vocabularycloses.skipped_reasonreuses the four valuesdoublegate_sdk.skippedcloses.excerptis bounded tomaxLength: 0— the reviewer input carries the deterministic evidence, never the body.
Everything else in those files — the member names, the nesting, the fact that an exchange has an "input" and an "output" at all — is this starter's invention. It is not what the product will publish, and building against it as if it were is the mistake this page exists to prevent. When a reviewer stage ships, delete the schemas and keep the plumbing.
Why the closed vocabularies are checked twice
The schema validates shape; the normalisers validate meaning. They disagree
usefully: a schema enum will happily accept null for skipped_reason, while
normalise_skipped_reason refuses None outright, because a skip that names no
reason is the silent skip that vocabulary exists to end. Running both is how you
find out that a verdict which validates would still be rejected by the SDK's
own normaliser.
Doing it in your own code
There is nothing to call that submits a verdict, so what follows is preparation only: the validators the SDK does publish, applied to a contract your application owns.
Derive real evidence for a reviewer input
from doublegate_sdk import evaluate_file
from doublegate_sdk.submission import (content_digest_from_content_hash,
envelope_content_hash)
evaluation = evaluate_file("gates/release.gate.json", "docs/release/2026.09.md",
artifact_type="memory")
reviewer_input = {
"artifact_type": evaluation.artifact_type,
"content_digest": content_digest_from_content_hash(
envelope_content_hash(open("docs/release/2026.09.md", "rb").read())),
"deterministic_outcome": evaluation.outcome, # 'clean' or 'flagged'
"human_review": evaluation.human_review, # still outstanding
"excerpt_policy": "withheld",
"findings": [finding.to_dict() for finding in evaluation.findings],
}
content_digest is derived by hashing the bytes once into the envelope
spelling and then prefixing. The SDK's submission profile forbids hashing
content twice to fill that member, which is why the two calls are chained rather
than envelope_content_hash being called again.
excerpt_policy is a constant, not a choice: findings from the bundled
evaluator carry fixed diagnostics and an empty excerpt, so the body never
travels with the input.
Validate a verdict against the SDK's closed vocabularies
from doublegate_sdk.reasons import CATEGORIES, normalise_category
from doublegate_sdk.schema import check_response
from doublegate_sdk.skipped import SKIPPED_REASONS, normalise_skipped_reason
from doublegate_sdk.vocabulary import BLOCKERS, normalise_blockers
violations = check_response(output_schema, verdict) # () when the shape is valid
category = normalise_category(verdict["category"]) # one of CATEGORIES
blockers = normalise_blockers(verdict["blockers"]) # a tuple from BLOCKERS
skipped = normalise_skipped_reason(verdict["skipped_reason"]) # one of SKIPPED_REASONS
check_response returns a tuple of violation strings — empty means valid — and
never raises for a merely invalid payload. The three normalisers raise
ValueError or TypeError instead, and that difference is the point of running
both: a schema enum will happily accept null for skipped_reason, while
normalise_skipped_reason refuses None outright, because a skip that names no
reason is the silent skip the vocabulary exists to end.
CATEGORIES, BLOCKERS and SKIPPED_REASONS are frozensets you can read
directly — use them to build your own schema enums rather than copying the
values by hand, so a future addition reaches your contract automatically.
What next. Nothing remote. There is no call that carries this verdict to a gate, and no admission follows from it. Keep the exchange in your own tests so that when a reviewer stage ships you are adapting working code.
Confirm the gap against your installed package
from doublegate_sdk.client import describe_client
description = describe_client()
sorted(description["operations"]) # ['pending', 'propose', 'recall', 'status', 'why']
description["mcp"]["tools"] # nine tools; none carries a reviewer verdict
Those five operations are the entire Python client surface in this build. Read them yourself instead of taking this page's word for it.
Exit statuses
| Status | Meaning |
|---|---|
0 |
the exchange satisfies the local contract |
1 |
it does not; violations are listed in the report |
3 |
--require-stage was given and no reviewer stage is published |
A missing or unusable fixture exits 1 with a one-line message naming the file;
argparse usage errors exit 2.
Completion status
App and tests are complete and pass, and every command on this page runs fully
offline against the installed SDK. Nothing here opens a socket, so there is no
real-service acceptance to record for this starter — the only thing a run can
establish is that the caller-owned contract in data/ holds against the SDK's
published validators and closed vocabularies. It is not evidence that a reviewer
stage exists, and not evidence that any verdict was admitted.