# Checkpoint Verdict Comment Contract

Canonical owner for gate-review **verdict field** rules for the two gate boundaries in
the dev-loop workflow: `draft_gate` and `pre_approval_gate`.

## Gate tiers (issue #1913)

The gate vocabulary is tiered, not flat. The discriminator is one question: **does it
block a lifecycle transition?** The canonical encoding lives in
`scripts/github/_gate-names.mjs` (`LIFECYCLE_GATES` / `REVIEW_GATE`).

| Gate | Tier | Blocks a transition? | Verdict evidence? |
|---|---|---|---|
| `draft_gate` | lifecycle | yes — draft→ready | yes (this contract) |
| `pre_approval_gate` | lifecycle | yes — ready→merge | yes (this contract) |
| `review` | informational | no — gates nothing | no (never satisfies lifecycle-gate evidence) |

A "run through the gates" / "gate this PR" request means the lifecycle gates, never the
informational `review` pass — see the review-intent short-circuit carve-out in
[Dev Loop Skill](../dev-loop/SKILL.md).

## Purpose

Gate-review verdicts make the workflow auditable and transparent from the PR
conversation alone. A reviewer or maintainer can inspect which gate ran, which head
commit was reviewed, whether it passed cleanly, and whether a result is current for
the latest head — without relying on local or session-only artifacts.

<!-- rule: GATE-COMMENT-SINGLE-SURFACE -->
`GATE-COMMENT-SINGLE-SURFACE`: A gate round produces exactly ONE new visible surface: a single PR
review of type COMMENT, posted by `upsert-checkpoint-verdict.mjs`. Its body carries the required
verdict fields below; with `--findings-ledger`, that same review also carries the round's
findings — locatable ones as its inline comments, the rest body-filed in full in the body's
bulleted list below; an invisible per-finding fingerprint+disposition marker, never rendered as
visible text, is what `GATE-EXEC-FINDING-THREADS`'s cross-round suppression/deferral tracking
actually reads back) ([Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md#finding-threads-and-disposition)).
No separate verdict issue comment, no separate findings review, and no deferred-summary comment
is posted. The body's per-angle breakdown is TWO TRACKS by locatability, rendered at TOP LEVEL,
NEVER a markdown table:
1. **Locatable findings** (each carried by its own inline PR review comment) are never
   enumerated per-finding in the body — no reference row, no restated text. The body states only
   one aggregate `**Inline findings:**` line: the count, a severity breakdown (leading emoji per
   the legend below, with the severity word), and the distinct touched angle names, pointing the
   reader to the inline comments on the diff.
2. **Non-locatable (body-only) findings** have no inline carrier, so the body is the only place
   their text lives: each renders in full as its own plain bulleted list item (never a table row),
   findings-first and severity-ordered, with a leading severity emoji marker (🔴 high · 🟠 medium ·
   🟡 low · ⚪ nit · 🔵 question, alongside the severity word), the finding's summary, its
   `file:line` linked to the blob at the reviewed head SHA when known, and its contributing
   angle(s) in trailing brackets.

Every clean (zero-finding) angle is collapsed into one trailing comma-joined `**Clean (N):**`
line, never a list/table row. A finding's full text always lives in EXACTLY ONE reader-reachable
carrier — its own inline review comment (locatable) or its own body-list bullet (non-locatable) —
never both, never neither, and never only the on-disk disposition ledger. Budget pressure on an
over-long round SHORTENS a body-only finding's rendered text rather than degrading it to an
omitted-count/ledger pointer. Both tracks render at TOP LEVEL, never through the
`--findings-summary`/`--findings-file` blockquoted continuation-line path below. Verdict evidence
is read from that review body; a verdict posted as an ISSUE comment still validates and is still
corrected on its own surface (back-compat read).

<!-- rule: GATE-EVIDENCE-AUDIT-TWO-SURFACES -->
`GATE-EVIDENCE-AUDIT-TWO-SURFACES`: any gate-evidence completeness audit or reporting path MUST
scan BOTH verdict surfaces — the PR-review stream (`pulls/<n>/reviews`, the primary surface per
GATE-COMMENT-SINGLE-SURFACE) and the visible issue-comment stream (`issues/<n>/comments`, the
back-compat read). Scanning the issue-comment stream alone reports a legitimately-posted
PR-review verdict as "missing": the post-drive audit that filed #1674 falsely concluded #1614's
round-2 `draft_gate` and `pre_approval_gate` verdicts were unposted because it read only
`issues/1614/comments`, where no verdict body lives (the verdicts existed as PR reviews at the
merged head). The deterministic post-drive audit helper is
`scripts/github/audit-gate-evidence.mjs` — it reads both surfaces through
`fetchGateEvidenceComments` and reports each gate's verdict as visible regardless of which
surface carries it, so a verdict posted only as a PR review is never reported missing. The
sanctioned poster never creates one. Two documented
exceptions exist: the opt-in findings comment (`gates.postFindingsComments`,
`GATE-COMMENT-IDENTITY-DISJOINT` below) adds a sanctioned second visible surface when a repo
opts in, and the zero-dep fallback poster
(`skills/dev-loop/scripts/post-gate-verdict-fallback.mjs`), used only when `@dev-loops/core`
is absent, posts a verdict issue comment the dev-loop skill documents as a degraded
audit-trail artifact.

<!-- rule: GATE-COMMENT-IDENTITY-DISJOINT -->
`GATE-COMMENT-IDENTITY-DISJOINT`: The verdict surface and the opt-in findings comment
(`gates.postFindingsComments`, `post-gate-findings.mjs` — the opt-in exception
`GATE-COMMENT-SINGLE-SURFACE` names) identify "their"
comment by different claim keys, and each tool's upsert MUST NOT ever claim the other's comment.
The verdict is claimed through its parsed verdict fields (gate name plus reviewed head); the
findings comment through its own `dev-loops:gate-findings gate=` marker. Enforced at the claim
seam by machine-artifact filtering plus verdict-body precedence: the marker summarizer treats a
body carrying a known machine-artifact marker token (owned by the artifact filter in
`copilot-helpers.mjs`, delimiter-anchored so no suffixed `<token>-<x>` variant matches) as a
non-candidate UNLESS it also carries the producer-owned verdict body heading — which is how the
round's own review, marker and all, stays claimable while the findings comment (which never
carries that heading) never is. That silent replacement previously destroyed a full round's
visible findings record seconds after it was posted. Within its OWN claim key each tool keys
identity as it needs (the findings comment's marker is deliberately gate-only).

<!-- rule: GATE-COMMENT-SCOPE-ONLY -->
`GATE-COMMENT-SCOPE-ONLY`: This document owns the visible checkpoint verdict evidence contract only.
It does not restate the full PR follow-up procedure; that
remains owned by the relevant workflow skill. The broader family-local PR lifecycle
that consumes this evidence is defined in [PR Lifecycle Contract](./pr-lifecycle-contract.md).

## Scope

This contract covers exactly two gates with distinct lifecycle semantics:

- `draft_gate` — **one-time transition boundary.** Runs right before `gh pr ready`
  (draft → ready-for-review boundary). Once a clean comment exists and the PR leaves
  draft, the gate is permanently satisfied; later head changes must not re-trigger it.
- `pre_approval_gate` — **recurring per-head gate.** Runs right before final approval /
  merge readiness on the current head SHA. A new pass is required for each new head
  after post-draft changes.

A THIRD gate, `review` (`GATE_NAMES`, `scripts/github/_gate-names.mjs`), exists
outside this contract's scope: a standalone, on-demand review pass reachable on
any PR with no lifecycle obligation of its own (see the
[Review skill](../review/SKILL.md)). It posts through the same single-surface
poster and required-fields shape this document defines, but is a deliberately
NON-EVIDENCE gate. The guard is authoritative recognition, not absence:
`review` IS a recognized gate name in the gate-comment header vocabulary a
comment is parsed against (`@dev-loops/core/github/copilot-helpers`), and
recognizing a comment's header as `review` is exactly what makes the parser
return non-evidence immediately — before it ever falls through to the
lenient whole-body `draft_gate`/`pre_approval_gate` token scan that a
genuinely unidentifiable comment relies on. That holds regardless of
`--findings-ledger`/the gate-findings-review marker. (`review` is also absent
from `GATE_CONFIG_KEY` in `@dev-loops/core/loop/gate-fanin`, but that is a
separate, unrelated fact — `review` has no `draft`/`preApproval`-style config
threshold — not the mechanism that keeps its comments from being misread as
draft/pre-approval evidence.) So a `review` comment never satisfies
`draft_gate` or `pre_approval_gate` evidence and `GATE-COMMENT-NON-
SUBSTITUTION` below applies to it symmetrically: a clean `review` comment
authorizes nothing this contract's two gates require.

<!-- rule: GATE-REVIEW-SUBMIT-MODES -->
### `review` gate submit modes (#1840)

`GATE-REVIEW-SUBMIT-MODES`: `upsert-checkpoint-verdict.mjs`'s `--submit
<pending|comment|request-changes|approve|discard>` flag is SCOPED TO `--gate
review` ONLY — passing it on `draft_gate`/`pre_approval_gate` is rejected with a
named error (never silently ignored); those two gates always submit a `COMMENT`
review per `GATE-COMMENT-SINGLE-SURFACE` (`GATE-COMMENT-NON-SUBSTITUTION`: a
clean pre-approval must stay a submitted, visible evidence surface, never
substitutable by a differently-submitted review).

| Mode | GitHub review `event` | Effect |
|---|---|---|
| `pending` | omitted | Creates an author-only draft review — invisible to other reviewers until a human submits it |
| `comment` (default when `--submit` is omitted) | `COMMENT` | Submits the review immediately (today's behavior, unchanged) |
| `request-changes` | `REQUEST_CHANGES` | Submits the review; a GitHub-native branch-protection signal that can BLOCK merge until dismissed |
| `approve` | `APPROVE` | Submits the review; a GitHub-native branch-protection signal that SATISFIES a required-approvals rule |
| `discard` | n/a (DELETE) | Deletes the caller's own pending draft review (`DELETE /pulls/<pr>/reviews/<id>`); leaves nothing behind |

`request-changes`/`approve` carry GitHub-native branch-protection effects
independent of any dev-loops gate. A headless/non-interactive review run
(`--auto`) is restricted to `pending`/`comment` — `--submit
approve`/`--submit request-changes`/`--submit discard` are REFUSED headless, so
automation can never auto-approve, auto-block, or auto-delete a PR review; they
are reachable only through the [Review skill](../review/SKILL.md)'s interactive
multiple-choice submit step.

Since #1888 that guarantee is STRUCTURAL, not caller self-identification:
the absence of `--auto` proves nothing (a headless caller can simply omit
the flag), so `approve`/`request-changes` (and `discard`, #1912) additionally
REQUIRE the explicit `--interactive-confirm` token — passed only by the review
skill's interactive submit step after a human made the choice — and are REFUSED
without it, both at CLI parse time and in the `upsertCheckpointVerdict()`
runtime entry (direct callers cannot bypass the CLI parser). `--auto` still
refuses those modes even WITH the token. Headless/agent callers may use
`--submit pending` or `--submit comment`.

Submit-existing-pending (#1912, part of `GATE-REVIEW-SUBMIT-MODES`): GitHub
allows only ONE pending review per user per PR, and that limit is PR-scoped, NOT
head-scoped — so ANY create (even a same-round `COMMENT` submit) returns HTTP
422 while a pending review exists on the PR, whatever head it sits on. When the
caller already has an own SAME-HEAD PENDING review, a
`--submit comment|request-changes|approve` re-run SUBMITS it via
`POST /pulls/<pr>/reviews/<id>/events` (mapped event, preserving the pending
review's inline comments) instead of POSTing a second review; `--submit discard`
DELETES it; `--submit pending` (leave-pending) leaves it in place (a noop). A
STALE own pending review on a DIFFERENT head is NOT this round's surface, so it
is DELETED before the round falls through to create a fresh review at the
current head (leaving it would 422 the create; submitting it would submit
stale-head content). `--submit discard` deletes the caller's own pending review
regardless of head. The own pending review is author-only, so its verdict
marker is not `visible` and the same-head marker scan
(`summarizeExistingComment`) cannot see it; the submit path detects it directly
off the raw reviews list (which returns a pending review only to its own author,
at most one per PR).

Separately, a `fanout_fanin` verdict posted with `--findings-json` but NO
`--findings-ledger` emits a one-line advisory warning naming `--findings-ledger`
as the missing inline-comment source: `--findings-json` alone renders body-filed
findings only, so that combination silently files ZERO inline comments. The
warning never blocks the post.

Every submit mode — including `approve` — stays a NON-evidence `review`
verdict for dev-loops gates: the authoritative-`review`-header guard above
reads only the comment body, never the review's `event`/`state`, so
`detect-checkpoint-evidence.mjs`/`detect-pr-gate-coordination-state.mjs`
report no draft/pre-approval evidence from it regardless of submit mode.

## Separate chains per gate

Each gate runs its own independent review chain (`GATE-EXEC-SEPARATE-CHAINS`, owned by
[Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md#separate-chains-per-gate)).
This section owns only the comment-visible ledger path per gate:

| Gate | Own disposition ledger path |
|---|---|
| `draft_gate` | `tmp/gate-findings/.../draft_gate-<sha>.json` |
| `pre_approval_gate` | `tmp/gate-findings/.../pre_approval_gate-<sha>.json` |

## Review-angle ownership and non-substitution rules

Each gate's review angles are defined in the project config (`gates.draft.angles` and `gates.preApproval.angles` in `.pi/dev-loop/defaults.yaml`). The reviewer persona for each angle is resolved via `resolveReviewerRole` from the gate's own angle entry, falling back to the built-in persona registry (`packages/core/src/config/config.mjs`). Consumer repos may override an angle's persona/prompt via its own `gates.<gate>.angles[]` entry in their config.

Resolve angles at runtime with `resolveGateAngles(config, "draft")` and `resolveGateAngles(config, "preApproval")` from `@dev-loops/core/config`. Do not hardcode angle names in skill procedures or review prompts.

| Gate | Boundary it governs | Review angles | What a clean comment authorizes | What it does **not** authorize |
|---|---|---|---|---|
| `draft_gate` | Draft → ready for review | Resolved from `gates.draft.angles` in config | `gh pr ready` / leaving draft for the reviewed head SHA | final-approval readiness, merge-ready claims, or satisfaction of `pre_approval_gate` |
| `pre_approval_gate` | Final approval / merge readiness | Resolved from `gates.preApproval.angles` in config | approval-ready / final-human-approval readiness for the reviewed head SHA | draft-stage `gh pr ready` decisions for a different gate run |

<!-- rule: GATE-COMMENT-NON-SUBSTITUTION -->
`GATE-COMMENT-NON-SUBSTITUTION`: A clean `draft_gate` comment does **not** satisfy `pre_approval_gate` requirements.
A clean `pre_approval_gate` comment does **not** retroactively replace the required `draft_gate` evidence for leaving draft.

## Required fields

<!-- rule: GATE-COMMENT-REQUIRED-FIELDS -->
`GATE-COMMENT-REQUIRED-FIELDS`: Every gate-review verdict body MUST include:

| Field | Description |
|---|---|
| **Gate name** | `draft_gate` or `pre_approval_gate` |
| **Head SHA reviewed** | The exact commit SHA that was reviewed |
| **Verdict** | `clean`, `findings_present`, or `blocked` |
| **Blocking severities** | (clean verdicts only) Which severity levels must be clean per gate config |
| **Findings summary** | Short truthful audit summary. Use `no issues found` only when the reviewed head needed no corrective change for that gate pass. |
| **Next action** | One of: `stay draft and fix`, `rerun gate`, `mark ready for review`, `await final human approval` |

## Optional size-budget fields

<!-- rule: GATE-COMMENT-SIZE-BUDGET-FIELDS -->
`GATE-COMMENT-SIZE-BUDGET-FIELDS`: `GATE-COMMENT-REQUIRED-FIELDS` above covers only the
fields every verdict body MUST carry. The size-budget merge gate
([Size-budget merge gate](./merge-preconditions.md#size-budget-merge-gate-issue-1480)) adds
three further fields that are OPTIONAL at the CLI layer. A `pre_approval_gate` verdict
renders them whether `--size-budget-json` is supplied explicitly or omitted: omitting it
auto-derives the size budget in-process (`evaluatePrSizeBudget` against the PR's base ref)
and fails closed with an actionable error, naming `--size-budget-json` as the escape hatch,
if the base ref or diff cannot be resolved — so a `pre_approval_gate` verdict never posts
with these fields absent. `draft_gate` and `review` verdicts do not auto-derive: they render
the fields only when `--size-budget-json` is supplied, omitted entirely otherwise. An
absent field reads as absent (`null`), never as a false negative:

| Field | Rendered line | Values |
|---|---|---|
| **Size-budget outcome** | `**Size-budget outcome:** <outcome>` | The recorded `gates.size` outcome, e.g. `pass`, `escalate`, `block` |
| **Size-budget T1 slice** | `**Size-budget T1 slice:** touched` or `**Size-budget T1 slice:** not touched` | Whether the diff touches the T1 tier |
| **Size-budget waiver** | `**Size-budget waiver:** none`, `**Size-budget waiver:** granted`, or `**Size-budget waiver:** granted by <approver>` | Whether a size-budget waiver was granted, and by whom if known |

All three fields are rendered together or not at all: they appear only when the outcome is
supplied, and are read back as `null` (not a parse failure) when the verdict body carries
none of them — the pre-size-budget comment shape stays valid evidence for every other rule
in this document.

These fields remain OPTIONAL at the CLI layer (`upsert-checkpoint-verdict.mjs` never
requires `--size-budget-json`), and the standard `pre_approval_gate` gate-verdict
procedure ([Copilot PR Followup](../copilot-pr-followup/SKILL.md)) supplies it explicitly on
every post as the preferred path, so a verdict produced through that procedure always
carries populated fields from the precomputed JSON. Even a `pre_approval_gate` verdict
posted with the flag omitted carries populated fields, via the in-process auto-derive
described above. A verdict read back with all three `null` — a `draft_gate`/`review`
verdict posted without `--size-budget-json`, or a pre-existing `pre_approval_gate` verdict
posted before this field set existed — is absent size evidence, which the size-budget
merge gate — consulted live by `buildPreMergeGateCheck`
(`scripts/github/detect-checkpoint-evidence.mjs`) on the authoritative pre-merge path, in
addition to `evaluateMergePreconditions`/the lifecycle state machine — reads as "human
approval required", never as a silent pass.

## Verdict definitions

<!-- rule: GATE-COMMENT-VERDICT-VALUES -->
`GATE-COMMENT-VERDICT-VALUES`: The verdict field MUST be one of the following values, each
with the fixed meaning below:

| Verdict | Meaning |
|---|---|
| `clean` | No findings with a severity in the gate's `blockCleanOnFindingSeverities` remain |
| `findings_present` | The gate found issues at blocking severities; fixes are required before the gate boundary can be crossed |
| `blocked` | The gate could not complete or a hard blocker prevented a verdict |

This rule is enforced at write time and at post time, not just documented:
`write-gate-findings-log.mjs` refuses a `--verdict` that contradicts the
`--findings`/`--findings-file` wrapper's `overallVerdict` before any ledger is
written, and `upsert-checkpoint-verdict.mjs` refuses a `--verdict` that
contradicts the consolidated ledger's `overallVerdict` for the same head and
gate (#1616). The consolidator (`consolidate-fanin.mjs`) already computes
`overallVerdict` from this rule's definitions; it threads through
`--ledger-out`'s `{ overallVerdict, findings }` wrapper into the durable ledger
(`write-gate-findings-log.mjs`), and `upsert-checkpoint-verdict.mjs` reads it
and derives the verdict by default (passing no `--verdict` is valid), accepts
a matching explicit value, and refuses a contradiction citing this rule. No
override flag — a round whose verdict genuinely differs from the computed one
is a consolidator bug to fix, not an operator decision to override.

`write-gate-findings-log.mjs`'s write-time contradiction refusal always
compares `--verdict` against the wrapper's `overallVerdict` — the
consolidator's computed round verdict — whether or not `--judge-verdict` was
also supplied. The judge only enriches findings with `act`/`defer`/`reject`
dispositions (see [Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md#phase-35--judge-relevance-disposition-1525));
it never revises the round verdict, so a `--judge-verdict` run is held to the
exact same contradiction check as a run without one.

## Disposition ledger

Durable-ledger sequencing and content are owned by `GATE-EXEC-DISPOSITION-LEDGER`
([Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md#disposition-ledger-and-durable-logging)).
The visible PR comment is a summary for auditability; the disposition ledger is the
complete durable record.

Disposing of the ledger's non-blocking findings as inline review threads on the round's own
review is owned by `GATE-EXEC-FINDING-THREADS` and `GATE-EXEC-THREAD-DISPOSITION`
([Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md#finding-threads-and-disposition));
a deferred finding's record is owned by `GATE-EXEC-DEFERRAL-RECORD` there too.

## Readable deterministic format

<!-- rule: GATE-COMMENT-VALIDATION-REPORTING -->
`GATE-COMMENT-VALIDATION-REPORTING`: Keep the visible verdict body compact, deterministic, and
slightly human-friendly (labels like `Gate review`, `Reviewed head SHA`, `Verdict`,
`Blocking severities`, `Findings summary`, `Next action`); gate name and reviewed head SHA
MUST stay deterministically parseable even if label wording changes. Validation reporting
MUST stay concise by default — command names plus pass/fail status, aggregate counts, and
current-head CI/check status, never raw passing log streams. Any included command output
MUST be truncated to a deterministic retained-prefix length (a short truncation marker
suffix is allowed); a failure MUST show only a focused relevant excerpt, not an unbounded
raw log dump. Detailed logs MAY live in local/session artifacts or linked GitHub logs
instead of the visible audit comment. When a pass reached `clean` only after corrective changes, the findings
summary SHOULD briefly say what gap was found, what changed, and why the current head now
satisfies the gate.

## Behavior requirements

Post-before-fix ordering is owned by `GATE-EXEC-POST-BEFORE-FIX`
([Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md#phase-3--consolidation-fan-in-synthesis-and-disposition-ledger));
it applies to both gate boundaries.

### Draft gate (`draft_gate`) comment requirements

<!-- rule: GATE-COMMENT-DRAFT-REQUIREMENTS -->
`GATE-COMMENT-DRAFT-REQUIREMENTS`: The PR MUST NOT leave draft unless a visible, current-head
`clean` `draft_gate` checkpoint verdict comment exists, per the rules below.

**One-time transition boundary.** `draft_gate` is not a recurring per-head gate — it
records exactly one decision point: the draft → ready-for-review transition. Once a
clean `draft_gate` comment exists on the PR and the PR leaves draft, later head
changes MUST NOT trigger new `draft_gate` comments. Post-draft follow-up relies on
normal review/fix loops and the recurring per-head `pre_approval_gate`.

- **Skip rule:** the skip applies only once the draft→ready transition is already
  recorded — a clean `draft_gate` comment exists on the PR (any head) AND the PR has
  already left draft. In that case, skip the draft gate entirely; do not re-post it on
  later heads. While the PR is still draft, a clean comment for an older head does not
  satisfy the current head — a new head requires a new current-head `draft_gate` comment.
- When the `draft_gate` runs (while the PR is still draft and no clean evidence exists
  for the current head), the PR MUST receive a visible checkpoint verdict comment.
- If the `draft_gate` verdict is `findings_present` or `blocked`, the comment MUST
  state that the PR stays draft and fixes are required before retrying.
- A checkpoint verdict comment for an older head SHA does not satisfy this requirement for
  the current head while the PR is still draft.
- After the PR leaves draft, existing clean `draft_gate` evidence remains valid as a
  one-time transition record — it records that the draft → ready boundary was properly
  crossed. Later head changes do not invalidate this record.
- If a PR is already non-draft and no clean `draft_gate` evidence exists at all (no
  valid checkpoint verdict comment was ever posted), automation MUST fail closed and reconcile
  that missing draft-stage evidence before continuing.

### Pre-approval gate (`pre_approval_gate`) comment requirements

<!-- rule: GATE-COMMENT-PREAPPROVAL-REQUIREMENTS -->
`GATE-COMMENT-PREAPPROVAL-REQUIREMENTS`: Final-approval readiness MUST NOT rely only on
local or hidden artifacts; a visible, current-head `pre_approval_gate` checkpoint verdict
comment is the required auditable evidence, per the rules below.

- When the `pre_approval_gate` runs, the PR MUST receive a visible checkpoint verdict comment.
- If the `pre_approval_gate` verdict is `findings_present` or `blocked`, the comment
  MUST state that follow-up fixes are required before final approval.
- A checkpoint verdict comment for an older head SHA does not satisfy this requirement for
  the current head.

## Rerun rules

<!-- rule: GATE-COMMENT-RERUN-RULES -->
`GATE-COMMENT-RERUN-RULES`: A gate rerun MUST follow the same-head vs. new-head handling
defined below, scoped per gate recurrence (`GATE-COMMENT-SCOPE-ONLY` above): this table
governs the **recurring** `pre_approval_gate`; the **one-time** `draft_gate` is exempt from
the new-head row once its one-time transition record exists (`GATE-COMMENT-DRAFT-REQUIREMENTS`)
so the two rules do not conflict.

| Scenario | Rule |
|---|---|
| Same head SHA rerun | Idempotent behavior: do not post a second visible surface for the same gate+head. An identical rerun posts nothing; if correction is needed, update the existing review's body in place (a legacy verdict issue comment is corrected on its own surface). Inline finding comments are never re-posted — a same-head correction body-files any still-unposted finding, since GitHub exposes no endpoint to add inline comments to a submitted review. |
| New head SHA rerun on the recurring `pre_approval_gate` | A new visible checkpoint verdict review MUST be posted for the new head; the older-head surface remains but does not satisfy readiness for the new head |
| New head SHA change on the one-time `draft_gate` after a clean transition record already exists | No new `draft_gate` verdict is triggered for the new head — the one-time transition boundary already closed (`GATE-COMMENT-DRAFT-REQUIREMENTS`) |

## Fail-closed behavior

<!-- rule: GATE-COMMENT-FAIL-CLOSED -->
`GATE-COMMENT-FAIL-CLOSED`: If the required checkpoint verdict review cannot be posted
(for example due to a GitHub API error, permission restriction, or tooling failure), the
workflow MUST NOT cross the gate boundary:

- do not run `gh pr ready` (for `draft_gate`)
- do not declare final-approval readiness (for `pre_approval_gate`)

The gate boundary is not crossed until both the review verdict is `clean` **and** the
required visible PR review is confirmed posted for the current head SHA.

## Relationship to other contracts

| Contract | Relationship |
|---|---|
| `draft_gate` boundary | Governs the draft → ready-for-review transition in [Copilot PR Follow-up](../copilot-pr-followup/SKILL.md) Step 7 |
| `pre_approval_gate` boundary | Governs final-approval readiness in [Copilot PR Follow-up](../copilot-pr-followup/SKILL.md) Step 7 and the narrowed [Final Approval](../final-approval/SKILL.md) route |
| Local/session artifacts | These remain complementary; the visible PR review is the minimum required auditable surface, not a replacement for all local artifacts |

## See also

- [PR Lifecycle Contract](./pr-lifecycle-contract.md) — broader lifecycle state machine
- [Checkpoint Review Chain Contract](./gate-review-sub-loop-contract.md) — execution shape for gate inspection work
- [Copilot PR Follow-up](../copilot-pr-followup/SKILL.md) — skill that owns gate execution
- [Final Approval](../final-approval/SKILL.md) — human approval gate route
- [Contract style guide](./contract-style-guide.md) — rule ID and RFC-2119 conventions
