kendex.ai

Marketplaces / vanillagreencom/kendex / review-gate

review-gate

Org-wide PR review gate: one predicate answers 'is this exact head reviewed?', one writer posts the answer as a merge-blocking commit status. Load to wire, adopt, tune, or debug a repo's gate or its REVIEW_GATE_* settings.

skill · review · safety 85/100 (warn) · @ a7ca9af

Install in kendex: kendex add --skill review-gate after subscribing to vanillagreencom/kendex.

Review Gate

Problem with this skill? Run kendex report — it files to the owning repo automatically. Do not hand-file.

The gate answers ONE question: has this exact PR head been reviewed? It posts that answer as a commit status the repo's branch rules require. It does not check CI, re-run anything, or reason about jobs.

Two greens do NOT mean a review happened. Under REVIEW_GATE_MODE = "off" the predicate evaluates no evidence and attests only that the repo disabled the gate; and merge-group statuses never read the mode, posting green as "merge-queue entry: post-approval by construction". Both: references/settings.md § REVIEW_GATE_MODE.

Decision table

VerdictStatusMeaning
approvedsuccessEvidence exists for this head; no standing objection; no unresolved threads. Under REVIEW_GATE_MODE = "off" the predicate evaluates NO term — success there means only "gate disabled", stated in the status description.
awaitingpendingNo review evidence for this head yet.
threads-openpendingEvidence exists, but review threads are unresolved.
changes-requestedfailureA reviewer objects. Red means objection — never a build failure.
(exit 2, no verdict)unchangedA read failed or config is invalid. Take NO action; retry next pass.

ADOPTION PRECONDITION — check this first

Whether untested code can reach the default branch is branch protection's job, so a repo must satisfy one of these or a reviewed PR merges untested: a merge queue whose required contexts include the test aggregate (recommended), or no held-back jobs. Held-back jobs report skipped, and GitHub counts skipped as satisfied. Wiring and the sandbox proof: references/adoption.md § The precondition.

Evidence sources (.agents/skills/review-gate/scripts/review-predicate.sh)

Evidence for the CURRENT head is any of:

  1. Review object at the exact head from a non-author, non-dismissed login — restricted to REVIEW_GATE_REVIEW_OBJECT_TRUSTED_LOGINS when set, and to APPROVED reviews when REVIEW_GATE_REVIEW_OBJECT_MIN_STATE = "approved". An approval is never superseded by a later COMMENTED from the same reviewer; only a later CHANGES_REQUESTED withdraws it. A row whose body matches REVIEW_GATE_REVIEW_OBJECT_ERROR_PATTERNS (an errored auto-review's own "unable to review" attestation — the reviews API has no errored state) is NOT-EVIDENCE, never a failure: the review-object mirror of the skip-pattern rule below.
  2. Trusted clean-analysis check-run or commit status (REVIEW_GATE_TRUSTED_STATUS_CONTEXTS) succeeding on this head — but a pass must prove analysis RAN: a success matching REVIEW_GATE_CHECKRUN_SKIP_PATTERNS (e.g. "rate limited") is NOT-EVIDENCE, never a failure. It is silence, which awaiting handles. On BOTH surfaces the NEWEST row/run per name decides (statuses by list order, check-runs by run id): an older clean success never outlives its reviewer's newer pending/failed/skip-marked round.
  3. Comment-form clean pass (REVIEW_GATE_COMMENT_REVIEWERS): an issue comment by a trusted bot login — never the PR author, even if configured — binding the evidence to this head's sha (floor REVIEW_GATE_SHA_PREFIX_FLOOR).
  4. Operator override (REVIEW_GATE_OVERRIDE_CONTEXT, legacy name REVIEW_GATE_OUTAGE_CONTEXT): a trusted operator's status carrying a NON-EMPTY reason, which is enforced and surfaced in the gate detail. Substitutes for MISSING evidence ONLY — it can never override a changes-requested or an unresolved thread, so the internal review must fix findings and resolve threads first, then attest.

With REVIEW_GATE_CARRY_FORWARD (off by default), evidence at an ancestor carries to head when the delta is provably in a class review would not re-examine — docs-only, comment-only, identical tree. Never a waiver: real evidence must exist, code changes always require fresh evidence, and the fail-closed terms still apply.

Changes-requested and unresolved threads always fail closed. Every evidence read fails LOUD (exit 2, no verdict) — a transient API error must never flip a healthy PR's state; reads retry in-process up to REVIEW_GATE_API_ATTEMPTS (default 1), and a zero-byte producer is a failed read, not an empty page set. Review threads are counted across pages (100 per page, bound 20 pages / 2000 threads); past the bound — or when pagination metadata cannot advance — the count reports overflow and fails closed to threads-open.

Trust model. Trust keys on names only GitHub controls: the author login of a review or comment, or the exact check/status context on repos where every publisher is trusted. A comment body never establishes trust — it is read only to BIND evidence to a commit, so a stale comment cannot vouch for a later push. Where PR workflows hold statuses:write, the opt-in REVIEW_GATE_STATUS_PUBLISHER_REJECT list rejects statuses minted by a forgeable creator (typically github-actions[bot]) on both the trusted-context and override reads. Statuses are read from the per-commit statuses LIST endpoint, where every real publisher — GitHub Apps included — carries a creator login; while the reject list is configured, a status with no creator login is an anomaly and is not evidence (with the list empty, the default, the filter is off entirely).

The writer (.agents/skills/review-gate/scripts/review-writer.sh)

One workflow, defined on the default branch, is the only thing that writes the gate status. It evaluates the predicate and converges the status — nothing else.

  • Converge-all on every leg. Every invocation converges EVERY open PR, so a run evicted from the concurrency group strands nothing (8 evictions observed in one sandbox replay, zero stranded).
  • Relay / converge split. PR-attached legs (pull_request_target, pull_request_review, status, an opted-in check_run) do NOT run the engine: they run a group-less relay job that dispatches a converge pass and exits. Only workflow_dispatch and schedule hold the single-writer group. Convergence is unchanged — what changes is WHERE an eviction's CANCELLED check lands: attached to a PR head it pinned that PR at mergeStateStatus UNSTABLE until a manual rerun, while nothing gates on the default-branch runs the relay dispatches into. It costs one non-evictable run per PR-attached event, coalescing nothing — size that before adopting on a capacity-limited runner pool (references/adoption.md § Updating an already-adopted copy).
  • The relay never exits non-zero — a pinned invariant, not a per-branch choice. It holds no statuses scope, so a failed dispatch cannot make the gate look converged — only leave it stale, which the cron floor and pr-watch --heal already own. Reddening would re-create the exact UNSTABLE pin the split removes. Every fault warns and exits 0, and every wait is bounded, so a sustained dispatch outage surfaces as gate staleness across every open PR rather than as N red PRs. The retry taxonomy and the bounds are pinned in adoption.md § Updating an already-adopted copy — read it before editing a consumer's copy.
  • Write ordering. Before any success post it re-reads the status and defers when any gate entry was created at/after this run's evaluation instant — a newer run's state AND description (which carries the audit detail) both stand; a failed re-read defers too. Downward posts never defer.
  • Idempotent. When the current entry already matches state + description it no-ops, so idle cron ticks append nothing.
  • Forks need no special case — every leg holds a write-capable default-branch token. The one exception is pull_request_review on a fork PR, whose token GitHub downgrades to read-only: it cannot dispatch, so that run exits green as a no-op and the cron floor converges it.
  • pull_request_target safety: the job never executes PR-controlled code. Every checkout pins the default branch with credentials dropped.

Scripts

# verdict for one head (env: GH_REPO, PR_NUMBER, HEAD_SHA[, PR_AUTHOR])
.agents/skills/review-gate/scripts/review-predicate.sh

# converge every open PR's gate status (env: GH_REPO)
.agents/skills/review-gate/scripts/review-writer.sh

# needs-attention reducer over open PRs (env: GH_REPO) — exit 0 nothing,
# 1 attention lines on stdout, 2 read errors. Flags: --heal (one writer
# dispatch on gate-stale, reported as an informational heal-dispatched
# line), --no-evaluate (predicate-skipping mode: thread,
# queue, and gate-status reads still run, so threads-open, disarmed, and
# the threads-driven gate-stale form all fire; verdict-driven forms need
# the predicate), --awaiting-after SECS (default: PR_REVIEW_WAIT_SECS)
.agents/skills/review-gate/scripts/pr-watch.sh [PR# ...]

# offline decision-table selftest (~1s, no network), run from the repo root
.agents/skills/review-gate/scripts/review-predicate-selftest.sh

# live replay against a throwaway sandbox repo — before any engine change
E2E_REPO=<owner>/<repo> .agents/skills/review-gate/tests/e2e-sandbox.sh

Operations

Watching one or many PRs without stalling. Never key a hand-rolled monitor on gate-state transitions — a PR sitting steadily at "pending with open threads" transitions nothing and you sleep through it. Run .agents/skills/review-gate/scripts/pr-watch.sh (optionally --heal) on the harness's wake-up mechanism instead: silence + exit 0 means nothing needs you; attention lines name exactly what does. See adoption.md § Watching PRs as an agent.

Reviewers are down / nothing is reviewing. Run the internal review loop: fix findings, resolve every thread, then post the override status with a real reason. It cannot bypass an objection or an open thread — that is the point.

A PR that repairs the gate itself. The writer always runs the merged engine, so a broken predicate cannot open its own repair PR's gate. Merge it with the ruleset's bypass actor (every fleet repo has one configured) and say so in the commit message.

A settings-change PR is judged by the OLD config — a PR adding a trusted login cannot have its own gate honor it. Merge via normal review or the bypass actor.

Settings

Every per-repo value is a REVIEW_GATE_* key (env > kendex.settings.toml > default). Full table and the security reasoning: references/settings.md.

Selftest — the engine's portable proof

.agents/skills/review-gate/scripts/review-predicate-selftest.sh pins the decision table offline, running the real predicate against the invoking repo's own resolved settings — so a repo trusting a different bot tests its own trust list. How it does that: DEVELOPMENT.md § How the selftest pins the decision table.

Every consumer's CI must run it as a deliberately ungated job (own job, no needs): a broken predicate approves nothing, so a selftest behind the gate could never run when it matters.

Adoption

Copy .agents/skills/review-gate/templates/review-gate-writer.yml into .github/workflows/ (repo-owned after copy; kendex refresh does not sync workflow YAML), set the repo's REVIEW_GATE_* values, add the ungated selftest job, and require the gate context in the ruleset alongside the test aggregate. Full wiring: references/adoption.md. An adoption PR deletes the local machinery it supersedes in the same PR — a redesign removes what it replaces, never leaves it dormant.

A consumer also merges re-vendor PRs whose entire delta is bytes already reviewed upstream. Suppress the duplicate findings with the remedy-locus reviewer instruction, never a reviewer path exclusion — excluding the vendored tree leaves a pure re-vendor PR with nothing to review, no review object, and a gate that starves: references/vendored-paths.md.