Contract
Contract lifecycle
When the discussion closes, the Relay's status becomes finalizing and the agents produce one structured technical contract. One agent writes it, both review the exact revision, and the owner approves it. This page follows that sequence.
PlanRelay checks the contract's structure and its references to recorded decisions. It does not check that the technical content is correct.
Phases and states
| contract_phase | Meaning |
|---|---|
| CONTRACT_AUTHORING | The author is writing or correcting the contract. Also set when a reviewer returns caveats or disagrees. |
| TECHNICAL_REVIEW | The latest revision has no validation errors and is waiting for reviews. |
| AWAITING_OWNER_APPROVAL | Both agents agreed on the same revision without caveats. The Relay status is agreed. |
| COMPLETE | The owner approved the specification. |
| plan.state | Meaning |
|---|---|
| DRAFT | Saved with validation errors. Not eligible for review. |
| UNDER_REVIEW | Saved with no validation errors. |
| APPROVED_BY_AGENTS | Certified: both agents submitted AGREE with no caveats. |
| REVIEW_REQUIRED | An owner directive requires a contract change. A new revision is needed. |
| OWNER_APPROVED | The owner approved this revision. It is never changed afterwards. |
1. Elect the Contract Author
Each agent nominates an author. Either role can be nominated.
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/contract-election" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"candidate": "B",
"reason": "Agent B owns the consumer and most of the new interfaces."
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "elect_contract_author",
"arguments": {
"candidate": "B",
"reason": "Agent B owns the consumer and most of the new interfaces."
}
}
}{
"ok": true,
"author": null,
"election": {
"A": {
"candidate": "B",
"reason": "Agent B owns the consumer and most of the new interfaces."
}
}
}candidateis"A"or"B".reasonis 12 to 2,000 characters.authorstaysnulluntil both agents have nominated.- When both nominate the same role, that role is the author.
- When they nominate different roles, the author is Agent A. The response records this under
election.fallback. - Once an author is set, further nominations return the existing result and change nothing.
The other agent is the Technical Reviewer. Both roles are in every state response as contract_author and technical_reviewer.
2. Read the schema
curl -sS "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/contract-schema" \
-H "Authorization: Bearer $PLANRELAY_TOKEN"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_contract_schema",
"arguments": {}
}
}The response has schema (the JSON schema), required_sections: 16, validator_version (currently 1.1.0) and validation_rules as text. A contract is one JSON object with these parts:
| Field | Contains |
|---|---|
| sections | Exactly 16 items: number (1–16), content (12–12,000 characters) and decision_ids. |
| tasks | At least one. id, agent, repo, status (VERIFIED_EXISTING, AGREED_TO_BUILD, OWNER_GATE, OUT_OF_SCOPE), modules, behavior, inputs, outputs, depends_on, gate_ids, completion, test_ids, interface_ids, decision_ids, evidence. |
| interfaces | At least one. id, provider, consumer, kind (API, WEBHOOK, EVENT, DATABASE, DATA), definition, auth, request, response, errors, idempotency, decision_ids. |
| tests | At least one. id, agents, scenario, expected, category (FUNCTIONAL, FAILURE, REGRESSION, JOINT, LIVE), gate_ids. |
| gates | id, owner, question, blocks (task ids), status (PENDING, SATISFIED), evidence, authorization (SPECIFICATION, IMPLEMENT, DEPLOY, ACTIVATE, LIVE_CHECK). |
| build_order | At least one phase: tasks and rationale. Earlier phases are built first. |
| deploy_order | At least one step: agent, repo, task_ids, prerequisites, flag, rationale, gate_ids. |
| authorization | Fixed values: specification_only: true and implementation_authorized, deployment_authorized, activation_authorized all false. A contract cannot authorize anything. |
The 16 sections
| Number | Title |
|---|---|
| 1 | Objective and end-to-end workflow |
| 2 | Architecture |
| 3 | Scope and exclusions |
| 4 | Verified capabilities |
| 5 | Ownership matrix |
| 6 | Builder requirements |
| 7 | Exact interfaces and data contracts |
| 8 | Lifecycle and failure recovery |
| 9 | Phases and dependency graph |
| 10 | Build order |
| 11 | Deployment order |
| 12 | Integration procedure |
| 13 | Acceptance and regression tests |
| 14 | Safeguards, monitoring and rollback |
| 15 | Owner decisions and authorization gates |
| 16 | Revision and review status |
Validation rules
Every saved revision is validated. The errors are returned as a list of strings in validation_errors. A revision with errors is saved as DRAFT and cannot be reviewed.
- Sections. Each number from 1 to 16 appears once. Content is at least 40 characters. Placeholder text (
TO_BE_CONFIRMED,TBD,unknown,not established,unchanged from,as agreed in turn) is accepted only when the section names a gate id. - Decisions. Every
decision_idsentry must be a decision whose status isLOCKEDorRESOLVED. Every such decision must be referenced at least once by a section, task or interface. - Tasks. Ids are unique.
repoequals the assigned agent's repository. Every referenced task, interface, test and gate exists. AnAGREED_TO_BUILDtask needs a build-order entry, a deploy-order entry, and at least oneFUNCTIONALand oneFAILUREtest. A dependency must be in an earlier build phase unless it isVERIFIED_EXISTING. AVERIFIED_EXISTINGtask needs file evidence. AnAGREED_TO_BUILDtask's gates include the gates of the tasks it depends on. A task with placeholder text needs a gate that blocks it. - Interfaces. Ids are unique. Provider and consumer are different agents, and each owns a task that references the interface. No placeholder text.
- Tests. Ids are unique. At least one test in each of
FUNCTIONAL,FAILURE,REGRESSIONandJOINT. ALIVEtest names a gate. - Gates. Ids are unique.
blocksnames existing tasks. ASATISFIEDgate has evidence. EverySPECIFICATIONgate isSATISFIED. There is at least onePENDINGgate for each ofIMPLEMENT,DEPLOYandACTIVATE. - Deploy order. The agent and repository match, and the agent owns every task in the step.
- Disagreements. None may be open.
Dry run
Check a draft without saving anything. The examples on this page abbreviate the contract object: each list is shown with one placeholder item.
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/contract/validate" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"contract": {
"sections": [
"…16 items"
],
"tasks": [
"…"
],
"interfaces": [
"…"
],
"tests": [
"…"
],
"gates": [
"…"
],
"build_order": [
"…"
],
"deploy_order": [
"…"
],
"authorization": {
"specification_only": true,
"implementation_authorized": false,
"deployment_authorized": false,
"activation_authorized": false
}
}
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "validate_contract_draft",
"arguments": {
"contract": {
"…": "the same contract object"
}
}
}
}{
"ok": false,
"validator_version": "1.1.0",
"validation_errors": [
"task T3: missing build order",
"tests: JOINT coverage required"
],
"note": "Dry run only — nothing was saved. Submit with submit_contract when clean."
}3. Preflight
The preflight maps every mission requirement to something in the plan. The requirements are the objective plus each line of the owner's questions to settle. Each has a stable id: R0 is the objective, R1 onward are the questions in order. Map by requirement_id. The exact text in requirement is still accepted, and an objective of any accepted length is valid here.
curl -sS "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/preflight" \
-H "Authorization: Bearer $PLANRELAY_TOKEN"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_preflight",
"arguments": {}
}
}{
"required": true,
"requirements": [
"Objective: Design how notifications consume checkout events.",
"Who owns the shared event?",
"What is the retry policy?"
],
"requirement_items": [
{
"id": "R0",
"text": "Objective: Design how notifications consume checkout events."
},
{
"id": "R1",
"text": "Who owns the shared event?"
},
{
"id": "R2",
"text": "What is the retry policy?"
}
],
"current": null,
"status": {
"required": true,
"state": "MISSING",
"ok": false,
"stored_ok": false,
"frozen": false,
"problems": [],
"unmapped_requirement_ids": [
"R0",
"R1",
"R2"
],
"problems_text": []
},
"limits": {
"max_mappings": 200
}
}The same requirement_items, and a certification_requirements summary of everything that must be true to certify, are in GET task (read_task) from the first call. Nothing else is demanded at sign-off. A Relay with more than 200 requirements is refused when the owner creates it.
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/preflight" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"mappings": [
{
"requirement_id": "R1",
"decision_ids": [
"c2a4e6f8-1b3d-4e5f-8a9b-0c1d2e3f4a5b"
],
"task_ids": [
"T1"
],
"test_ids": [],
"gate_ids": [],
"note": ""
}
]
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "submit_preflight",
"arguments": {
"mappings": [
{
"requirement": "Who owns the shared event?",
"decision_ids": [
"c2a4e6f8-1b3d-4e5f-8a9b-0c1d2e3f4a5b"
],
"task_ids": [
"T1"
]
}
]
}
}
}{
"ok": false,
"errors": [
"requirement not mapped: R0 \"Objective: Design how notifications consume checkout events.\"",
"requirement not mapped: R2 \"What is the retry policy?\""
],
"requirements": [
"…"
],
"state": "FAILING",
"problems": [
{
"requirement_id": "R0",
"problem": "UNMAPPED"
},
{
"requirement_id": "R2",
"problem": "UNMAPPED"
}
]
}- Send 1 to 200 mappings.
requirementmust equal the requirement text; letter case and spacing are ignored. - Each mapping needs at least one decision, task, test or gate id.
- Each decision id must be
LOCKEDorRESOLVED. - Once an author is elected, only the author can submit (
403 NOT_CONTRACT_AUTHORotherwise). - You can submit at any time, including during the discussion. The submission is stored either way.
okis recalculated on every read from the stored mappings and the current decisions. It becomes true when the decisions it names are accepted, and stops being true when one is rejected, withdrawn or superseded (stateSTALE).current.stored_okis the result at submission. An owner-approved specification keeps the result it was approved with.- When
requiredis true and the preflight is notokduring finalization, certification is blocked by aPREFLIGHT_INCOMPLETEblocker that lists each requirement id and the reason inproblems.
4. Submit a revision
Only the elected author submits. Every submission creates a new immutable revision.
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/contract" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"contract": {
"sections": [
"…16 items"
],
"tasks": [
"…"
],
"interfaces": [
"…"
],
"tests": [
"…"
],
"gates": [
"…"
],
"build_order": [
"…"
],
"deploy_order": [
"…"
],
"authorization": {
"specification_only": true,
"implementation_authorized": false,
"deployment_authorized": false,
"activation_authorized": false
}
},
"expected_parent_id": null,
"reason": "First complete contract from the agreed decisions."
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "submit_contract",
"arguments": {
"contract": {
"…": "the same contract object"
},
"expected_parent_id": null,
"reason": "First complete contract from the agreed decisions."
}
}
}{
"ok": true,
"revision_id": "0b7c2d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"version": 1,
"validation_errors": [],
"diff": [
{
"collection": "sections",
"key": "1",
"change": "added"
}
],
"diff_text": "- ADDED sections 1\n…",
"instruction": "Exact revision ready for technical review by both agents."
}| Field | Type | Required | What it does |
|---|---|---|---|
| contract | object | required | The complete contract. It must match the schema, or the request returns 400 INVALID_INPUT and nothing is saved. |
| expected_parent_id | uuid | null | required | The revision_id of the revision you are replacing, as returned by GET plan. Send null only when no revision exists yet. |
| reason | string, 12–2,000 | required | Why this revision exists. Stored with the revision. |
expected_parent_id and stale revisions
expected_parent_id must be the latest revision at the moment the request is processed. If another revision was saved in between, nothing is saved and the response is:
{
"ok": false,
"error": "REVISION_CONFLICT",
"message": "Contract action rejected; inspect validation errors and current revision.",
"current_revision_id": "1c8d3e2f-4a5b-4c6d-9e7f-8a9b0c1d2e3f",
"current_version": 2
}Read GET plan again, apply your change to that revision, and resubmit with current_revision_id as the parent.
What a new revision changes
- Reviews do not carry over. Both agents review the new revision from the start.
- With no validation errors the revision is
UNDER_REVIEWand the phase isTECHNICAL_REVIEW. With errors it isDRAFTand the phase staysCONTRACT_AUTHORING. - A revision the owner already approved is never modified. A correction is always a new revision.
POST plan (submit_position) and POST plan-revision (propose_revision) run the same operation with the same fields.
Patching instead of resubmitting
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/contract/patch" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"expected_parent_id": "0b7c2d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"reason": "Add the missing failure test for T3.",
"ops": [
{
"op": "add",
"collection": "tests",
"value": {
"id": "TEST-9",
"agents": [
"B"
],
"scenario": "Publishing fails after payment capture.",
"expected": "The event is retried three times, then written to the dead-letter queue.",
"category": "FAILURE",
"gate_ids": []
}
},
{
"op": "replace",
"collection": "sections",
"key": "13",
"value": {
"number": 13,
"content": "…",
"decision_ids": [
"c2a4e6f8-1b3d-4e5f-8a9b-0c1d2e3f4a5b"
]
}
}
]
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "patch_contract",
"arguments": {
"expected_parent_id": "0b7c2d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"reason": "Add the missing failure test for T3.",
"ops": [
{
"op": "remove",
"collection": "tests",
"key": "TEST-2"
}
]
}
}
}opsholds 1 to 50 operations.opisadd,replaceorremove.collectionis one ofsections,tasks,interfaces,tests,gates,build_order,deploy_order.keyidentifies the item forreplaceandremove: the section number for sections, theidfor tasks, interfaces, tests and gates. Omit it foradd.valueis the whole item foraddandreplace.- The patched contract is validated against the schema and then saved as a new revision, with the same response as a full submission.
GET contract/diff?revision_id=… (get_revision_diff) returns diff and diff_text for a revision against its parent. POST contract/revalidate (revalidate_contract) re-runs validation on the latest revision, or on revision_id if you send one, and stores the run.
5. Review the exact revision
Both agents, the author included, review the latest revision. A review names the revision id and its content hash, so it cannot apply to anything else.
- Read the revision:
GET planorget_final_plan. Noterevision_idandcontent_hash. - Check the contract against your own repository.
- Submit your verdict.
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/final-review" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"revision_id": "0b7c2d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"content_hash": "9f2c…(64 hex characters)",
"verdict": "AGREE_WITH_CAVEATS",
"caveats": [
{
"text": "Interface IF-2 does not state the error body for a 409."
}
]
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "submit_final_review",
"arguments": {
"revision_id": "0b7c2d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"content_hash": "9f2c…(64 hex characters)",
"verdict": "AGREE",
"caveats": []
}
}
}{
"ok": true,
"certified": false,
"revision_id": "0b7c2d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"version": 1,
"corrected": false,
"instruction": "Continue authoring/corrections/review; no exit before certification."
}| Field | Type | Required | What it does |
|---|---|---|---|
| revision_id | uuid | required | The revision you reviewed. |
| content_hash | string, 64 characters | required | The SHA-256 of that revision's rendered contract, from GET plan. |
| verdict | "AGREE" | "AGREE_WITH_CAVEATS" | "DISAGREE" | required | Your verdict. |
| caveats | object[], up to 15 | depends | Items with text (1–2,000). Required for AGREE_WITH_CAVEATS and DISAGREE. Must be empty for AGREE. |
Certification
A revision is certified when both agents have submitted AGREE with no caveats on it. The response then has certified: true, the revision state becomes APPROVED_BY_AGENTS, and the phase becomes AWAITING_OWNER_APPROVAL. The agents then wait. Certification is not permission to implement.
What a review is checked against
- The id and hash must be the latest revision:
409 STALE_CONTRACT_REVISIONotherwise, withcurrent_revision_id. - The revision must have a contract with no validation errors:
409 CONTRACT_INVALID. - No blocking item may be open:
409 OPEN_BLOCKERSwith the list inblockers. - Before certification you can change your review by submitting again; the earlier vote stays in the audit log. After certification a change returns
409 REVIEW_IMMUTABLE.
Findings
Findings are the channel between the Technical Reviewer and the Contract Author. They are separate from turns and never use one.
curl -sS -X POST "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/findings" \
-H "Authorization: Bearer $PLANRELAY_TOKEN" \
-H "content-type: application/json" \
-d '{
"kind": "PATCH_REQUEST",
"blocking": true,
"target": "T3",
"text": "Task T3 has no FAILURE test for a failed publish.",
"evidence": [
{
"repo": "notifications",
"path": "src/consumers/order.ts",
"level": "VERIFIED_IN_CODE"
}
]
}'{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "submit_finding",
"arguments": {
"kind": "FINDING",
"blocking": true,
"target": "T3",
"text": "Task T3 has no FAILURE test for a failed publish."
}
}
}{
"ok": true,
"finding_id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d"
}| Field | Type | Required | What it does |
|---|---|---|---|
| text | string, 12–4,000 | required | The defect or the change you request. |
| kind | "FINDING" | "PATCH_REQUEST" | optional | Defaults to FINDING. |
| blocking | boolean | optional | Whether it blocks certification while open. Defaults to true. |
| target | string, up to 200 | optional | The section number or task, interface, test or gate id it is about. |
| evidence | object[], up to 10 | optional | Evidence items. |
| revision_id | uuid | optional | The revision it applies to. Defaults to the latest revision. |
- The reviewer raises it. The author cannot raise findings (
403 AUTHOR_CANNOT_RAISE). Its status isOPEN. - The author responds with
POST findings/respond:finding_id,decision(ACCEPTEDorREJECTED),response(8 to 4,000 characters) and, optionally, therevision_idthat carries the fix. The response never closes the finding. Whoever raised it gets a pending action to verify. - For an accepted finding, the author ships the fix in a new revision. The author can respond again with
ACCEPTEDand thatrevision_id. A new revision also queues afinding_verificationaction for the reviewer of each accepted finding. - Whoever raised it verifies and closes it with
POST findings/resolve:finding_idandoutcome(RESOLVEDorWITHDRAWN). A moderator closes its own findings withPOST moderator/findings/resolve.
A finding with blocking: true blocks certification while its status is OPEN or ACCEPTED. GET findings (list_findings) returns every finding, including those a moderator raised, each with an awaiting field that says who must act next.
| awaiting | Who must act |
|---|---|
| AUTHOR_RESPONSE | The Contract Author: respond to an OPEN finding, or ship the revision for an ACCEPTED one. |
| REVIEWER_VERIFICATION | Whoever raised it: the author answered and there is a revision or a rejection to check. |
| OWNER_DECISION | The owner: the reviewer can no longer act (stopped moderator, lapsed plan, revoked or expired access). The owner dismisses it with a reason or brings the reviewer back. |
| NONE | Nobody. The finding is RESOLVED, WITHDRAWN or DISMISSED. |
Blockers
curl -sS "https://planrelay.io/api/public/agent/sessions/$SESSION_ID/contract-blockers" \
-H "Authorization: Bearer $PLANRELAY_TOKEN"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_blockers",
"arguments": {}
}
}{
"blockers": [
{
"code": "REVIEW_FINDING_OPEN",
"blocking": true,
"record_id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
"record_type": "review_finding",
"state": "OPEN",
"reason": "Task T3 has no FAILURE test for a failed publish.",
"resolution": "Contract Author responds (respond_finding), then the raiser resolves it.",
"actor": "AUTHOR",
"severity": "MEDIUM",
"raised_by": "B",
"new_evidence": false,
"what": "Blocking MEDIUM finding raised by Agent B: \"Task T3 has no FAILURE test for a failed publish.\" — the Contract Author has not responded.",
"owner": "Contract Author (Agent A)",
"next_action": "POST findings/respond (MCP respond_finding) with finding_id …, decision ACCEPTED or REJECTED, and response. If a revision already fixes it, add revision_id. Agent B then verifies.",
"evidence_required": "A response that addresses the finding: the fix (and the revision that carries it), or the reason it does not apply. The author's response never closes the finding.",
"category": "TECHNICAL_DEFECT",
"awaiting": "AUTHOR_RESPONSE"
}
],
"blocking_count": 1,
"preflight": {
"required": true,
"ok": true,
"state": "OK",
"problem_count": 0
},
"note": "Computed from current records on every call; nothing needs to be revalidated by hand."
}The list is computed from the current records on every call. When a finding is resolved, a hold is released or expires, a question is answered, a decision changes or a preflight is submitted, the next read reflects it. There is no revalidation step.
Each item carries four explicit fields: what (what failed or is missing, with the specifics), owner (who must act: Agent A, Agent B, Contract Author (Agent X), Moderator name, or Owner), next_action (the exact REST route and MCP tool, or the owner's action) and evidence_required. The earlier fields (code, record_id, reason, actor, resolution, state) are unchanged. Items with blocking: true stop reviews and owner approval. A review rejected with 409 OPEN_BLOCKERS returns the same items in blockers.
| category | Meaning |
|---|---|
| TECHNICAL_DEFECT | The engineering content is disputed or has a defect. |
| MISSING_EVIDENCE | An answer, a contract or a mapping has not been supplied. |
| AGENT_AVAILABILITY | Whoever must act cannot: access revoked, never joined, token expired, moderator stopped. Routed to the owner. |
| ENVIRONMENT | Reserved for conditions outside the Relay. |
| OWNER_DECISION | Only the owner can settle it. |
| PLATFORM_VALIDATION | A PlanRelay validation rule fails; the item lists the failing rules. |
| ADMINISTRATIVE | A bookkeeping step, such as acknowledging a directive. |
| code | Blocking | Open item |
|---|---|---|
| OPEN_QUESTION | yes | A question between the agents, or from the owner, has no answer. |
| OPEN_DISAGREEMENT | yes | A disagreement is not resolved. |
| OWNER_QUESTION_PENDING | yes | A targeted agent has not answered a blocking owner question. |
| OWNER_DIRECTIVE_PENDING | yes | A targeted agent has not acknowledged a blocking owner directive. |
| MODERATOR_QUESTION_PENDING | yes | A targeted agent has not answered a blocking moderator question. |
| DECISION_NOT_RESOLVED | no | A decision is PROPOSED, DISPUTED or OWNER_REQUIRED. Listed for information. |
| CONTRACT_MISSING | yes | No structured contract exists yet. |
| CONTRACT_INVALID | yes | The latest revision has validation errors. The item lists them in validation_errors (first 20) and follows the decision ledger: it is recalculated when decisions change. |
| CONTRACT_REVIEW_REQUIRED | yes | An owner directive requires a contract change. It clears by itself if the owner withdraws the directive. |
| CONTRACT_RECERTIFICATION_REQUIRED | yes | A decision or disagreement changed after the revision was certified. Its certification was withdrawn; the author submits a new revision and both agents review it. |
| PREFLIGHT_INCOMPLETE | yes | The preflight is required and is missing, incomplete or names a decision that is no longer accepted. problems lists each requirement id and why. |
| REVIEW_FINDING_OPEN | per finding | A finding is OPEN or ACCEPTED. Blocking follows the finding's own blocking flag. |
| CERTIFICATION_HOLD | yes | A moderator's hold is active, not expired, and its finding is still open. |