Review and merge#

The reviewer binds a GitLab merge request, source branch, and full commit SHA before it contacts a model. It runs from a disposable detached worktree at that SHA, so an uncommitted change in the caller’s checkout cannot reach model tools or the permanent GitLab record.

# Review the immutable merge-request base and head, then publish to that MR.
gate review

# Emit a stable, pipeable result. Progress remains on stderr.
gate review --json | jq .

# Inspect native GitLab thread state for the exact current head.
gate findings
gate findings --ack abcdef123456 --reason 'tracked in APPS-97'
# Reply with the verified repair or rejection in GitLab, then resolve.
gate findings --resolve abcdef123456

# Disposition a finding after the branch landed and was deleted: name the MR.
gate findings --mr 44 --resolve abcdef123456

# Review, publish, inspect blocking discussion state, then land the reviewed SHA.
gate merge agent/APPS-97
gate merge --explain

gate merge is blocking. It requires a complete GitLab read-back receipt and refuses a branch that moves after review. It selects a landing adapter once: worktree when the fleet lifecycle is available, otherwise corpus-free plain (git + glab). Use --land-adapter to select one explicitly; --explain reports the selected mechanism. Project configuration uses gate.toml with [land] and adapter = "plain" or adapter = "worktree"; explicit flags override $GATE_LAND_ADAPTER, which overrides that project setting.

review and findings return 0 when their command result is published, cached or available, and 2 when it is unavailable. Only gate merge returns 1, when a completed review blocks landing on current-head findings; an unavailable review or unverified landing returns 2. A zero review exit does not resolve discussions or establish investigation completeness: inspect the JSON status, findings and durable receipt. By default, an eligible review failure can be published as unreviewed and still return zero; that record discloses a failed investigation. GATE_FAIL_CLOSED=1 prevents that failure-publication path from admitting an unreviewed result. A fresh gate review retries a previously disclosed failure instead of treating it as a completed investigation.

Within the Corpus fleet, use agent-worktree merge to land the reviewed branch. It calls Gate review and verifies the receipt; it does not invoke gate merge. The standalone gate merge examples above describe Gate’s own landing command.

What Gate reviews#

Every changed path classifies into exactly one of five classes (gate/review/scope.py), and only one of them is ever critiqued:

  • Code – reviewable. Configuration stays code: .py, .typ, .yaml, .toml, .json, and anything else not claimed by the four classes below.

  • Prose – .md, .mdx, .rst, .txt, .adoc by default, plus gate.toml’s [review] prose = [...] glob list.

  • Content – archival or data text opted in per project through [review] content = [...]; empty by default.

  • Generated – lockfiles, minified bundles, and snapshots (*.lock, package-lock.json, *.min.js, *.snap, *.svg, and similar).

  • Binary – decided by Git’s own classification, never by extension.

Prose, content, generated, and binary paths are identified, never served: read_file, git_show, and git_diff return an identity stub instead of bytes, grep counts a match inside one in a notice rather than showing the line, an unnarrowed range diff excludes them by pathspec before Git ever assembles the patch text, bundle’s rung-1 fallback lists them by name, and the coverage contract owes no line inside them. Generated paths keep one deliberate, older exception: a git_show or git_diff narrowed to a generated path by name still returns its patch, because a complete lockfile change under bundle’s 4,000-byte threshold is evidence of what a dependency bump did, not churn, and rung 1 inlines exactly those through the same narrowed request. Prose and content earn no such exception, narrowed or not. That a path changed is signal; its contents are not evidence. When every changed path falls outside the code class, gate review never contacts a model at all: it returns looks-solid at rung 0 with a reason naming the count in each class, and zero gateway requests. The secret scan over the immutable patches still runs first and still blocks – a prose-only range with a committed secret is not exempted by this policy. Every review’s receipt – the durable record, --json, and terminal output – names every out-of-scope path and its class in unreviewed_paths, the way binary_paths already does. After a failed immutable preflight the same contract holds for whatever inventory the preflight classified before dying: nothing was served, so a prose or content path named there reads as unreviewed, never as reviewed-by-omission. A failure that struck before any path was classified carries no inventory, and that receipt’s empty unreviewed_paths means unknown, not none – treat the whole range as unexamined.

[review].prose and [review].content in gate.toml are additive glob lists layered on top of the defaults above, matched against the full repo-relative path (so content = ["archive/**"] selects a whole directory, and a bare *.rfc still matches at any depth); a non-list value fails the review closed, naming the config path in the message, rather than silently reviewing (or silently skipping) a path a maintainer wrote wrong. Gate reads gate.toml from the merge request’s immutable base revision, never from the head under review: a change cannot widen its own [review] globs and use that same change to exempt itself from review.

Model input retains the full textual diff so committed-secret findings remain detectable. Git-classified binary paths have no text lines to scan: Gate records them in the review’s binary_paths receipt (including durable state and --json) and names them in terminal output, while binary omission alone does not block an otherwise clean verdict. Classification follows Git rather than the extension or presumed human readability, so UTF-16 and other NUL-bearing files Git reports as binary follow this policy too. The path inventory remains bounded. An over-cap, timed-out, failed, NUL-bearing, or undecodable patch Git reports as nonbinary still fails closed before any provider call. Before model prose reaches GitLab, publication redacts credential-shaped material and blocks secret-bearing file anchors. The local cache is not a disposition ledger: acknowledgement and resolution live in GitLab discussion threads and are read back before a merge. --ack requires --reason and posts it as an acknowledgement without closing the thread. --resolve only changes native resolution state; a supplied --reason is ignored on that path. Post the repair or rejection as a discussion reply before resolving it.

An emergency --no-review request is deliberately unavailable until a durable Plane-ready outbox is installed. No bypass can silently land a branch.