reviewgate

package
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package reviewgate is the review gate (Task 20357): an optional reviewer model, configured per project and free to differ from the model that does the work, that checks each task's changes before cloop lets them leave the working copy.

What "before" means here

A task's work leaves the machine in three ways, and the gate stands in front of each:

  • a merge cloop performs itself — git mode merging a task branch, the parallel merge queue landing a worktree branch — which the orchestrator simply does not perform until the review approves, and then only if the tree it is about to merge is the tree that was reviewed;
  • a push the agent performs during its turn, which is the common case: a project's constraints say "commit and push after each task", and a granted repository is only worth anything once pushed. Those pushes are held (hold.go) and replayed by cloop after the approval (publish.go);
  • nothing at all, for a project that only works locally. The review still runs, and in fix mode still sends the agent back, but there is nothing to hold.

Holding a push without touching the repository

The agent's environment gets `url.cloopgate::.pushInsteadOf` through git's GIT_CONFIG_COUNT protocol, which rewrites every push URL to `cloopgate::<url>`. git then runs `git-remote-cloopgate`, a two-line script on the agent's PATH that execs `cloop review-gate-remote-helper`. The helper speaks git's remote-helper protocol just far enough to be handed the refspecs, records them, and refuses each with "held by cloop's review gate". Nothing is written into the repository — no hook, no config — so a user's own hooks keep running, and the rewrite ends with the agent's process. Fetches are untouched: pushInsteadOf applies to pushes only.

This is a guardrail against an agent following its instructions to push, not a boundary against one that wants to evade it: an agent can unset the variables or talk to the forge's API directly. The boundary for isolated executors remains the git proxy, which enforces branch restrictions outside the sandbox.

The invariant publishing keeps

cloop never pushes a commit the reviewer did not see. A held push is replayed only if every commit it would send — its commits not yet on the remote, by the remote-tracking refs — lies in the range the review covered, and it is replayed at the reviewed commit rather than at whatever the branch points to by then. The review range is chosen to make that hold in the ordinary case: when a push was held, the diff starts where the remote's copy of the branch forks from HEAD, so earlier unpublished commits are reviewed with the task's own rather than slipping out behind them.

Index

Constants

View Source
const DefaultTimeout = 30 * time.Minute

DefaultTimeout bounds one review call when the project sets no step timeout. A reviewer reading files for context takes minutes; one that has not answered in half an hour is not going to.

View Source
const HelperSubcommand = "review-gate-remote-helper"

HelperSubcommand is the hidden cloop subcommand that serves the transport.

View Source
const HelperTransport = "cloopgate"

HelperTransport is the git transport name held pushes are rewritten to. git runs "git-remote-" + HelperTransport for a URL of the form "cloopgate::<url>".

View Source
const HoldFileEnv = "CLOOP_REVIEW_GATE_HOLD"

HoldFileEnv names the file the helper records held pushes in. It is set in the agent's environment only; the helper refuses to run without it.

Variables

View Source
var ErrNoVerdict = errors.New("the reviewer's answer contained no verdict")

ErrNoVerdict is returned when a reviewer's answer contains no verdict.

Functions

func BuildPrompt

func BuildPrompt(in Input) string

BuildPrompt renders a review round's prompt.

func FixPrompt

func FixPrompt(reviewer string, v *Verdict, round, maxRounds int, holding bool, resumed bool, original, previous string) string

FixPrompt is what an agent is told when the reviewer requests changes and it is sent back to address them. resumed says whether it continues the agent's own conversation; when it does not, original and previous carry the task prompt and the agent's last answer.

func PromptSection

func PromptSection(mode string, holding bool) string

PromptSection is appended to a task's prompt while the gate is on, so the agent knows its pushes are held and why a push reports "rejected".

func Publish

func Publish(ctx context.Context, reviewed *Changes, pushes []HeldPush) []pm.ReviewPublish

Publish replays the pushes a review approved.

Each push is sent at the commit the review saw — its source ref as it stands now, which is where the agent's fixes are, provided that is still the reviewed HEAD or behind it — and only if every commit it would deliver lies in the reviewed range. A push that fails that check is refused, not sent: an approval covers what the reviewer read, and nothing else.

func ServeRemoteHelper

func ServeRemoteHelper(ctx context.Context, args []string, holdFile string, in io.Reader, out io.Writer) error

ServeRemoteHelper answers git's remote-helper protocol for a push made under the review gate. args are git's (the remote name and URL). It advertises the remote-tracking refs as the remote's state, so git's own fast-forward and lease checks still run against what the agent last fetched; records each ref update git then asks for in the hold file; and refuses every one of them with heldReason.

It holds rather than pushes by construction: nothing here can reach a remote, so a helper run outside a hold — HoldFileEnv unset — has nowhere to record to and refuses outright.

func Toplevel

func Toplevel(ctx context.Context, dir string) string

Toplevel returns the top-level directory of the repository containing dir, or "" when dir is not in one.

func TreeOf

func TreeOf(ctx context.Context, dir, rev string) string

TreeOf returns the tree of commit-ish rev in dir, or "".

func Withhold

func Withhold(root string, pushes []HeldPush, why string) []pm.ReviewPublish

Withhold records held pushes as not sent, with why.

Types

type Changes

type Changes struct {
	Root  string
	Repos []RepoChanges
}

Changes is what a task changed, per repository.

func Collect

func Collect(ctx context.Context, snap *Snapshot, held []HeldPush) (*Changes, error)

Collect gathers what changed since snap in every repository under the project, including repositories the task created or cloned. held are the pushes the agent attempted; they widen a repository's range to everything that push would publish.

func (*Changes) Empty

func (c *Changes) Empty() bool

Empty reports whether no repository has anything to review.

func (*Changes) Repo

func (c *Changes) Repo(dir string) *RepoChanges

Repo returns the entry for the repository at dir, or nil.

type HeldPush

type HeldPush struct {
	// Repo is the top-level directory of the repository pushed from.
	Repo string `json:"repo"`
	// Remote is the remote name git was given, or the URL when the push
	// named one directly. URL is where git would have pushed.
	Remote string `json:"remote"`
	URL    string `json:"url,omitempty"`
	// Src is the source as git passed it ("HEAD", "refs/heads/main"); empty
	// for a deletion. SrcRef is the ref it named when held ("" when it named
	// a commit), SrcSHA the object it resolved to then.
	Src    string `json:"src,omitempty"`
	SrcRef string `json:"src_ref,omitempty"`
	SrcSHA string `json:"src_sha,omitempty"`
	// Dst is the ref on the remote ("refs/heads/main").
	Dst string `json:"dst"`
	// Force reports a forced update; Expect is the value git was shown for
	// Dst, which a replay leases against.
	Force  bool   `json:"force,omitempty"`
	Expect string `json:"expect,omitempty"`
	// At is when git asked.
	At time.Time `json:"at"`
}

HeldPush is one ref update the agent pushed while the gate held it.

func Dedupe

func Dedupe(pushes []HeldPush) []HeldPush

Dedupe returns held pushes with only the latest attempt per repository, remote and destination ref, in the order they were first made. An agent that pushed, fixed a finding and pushed again meant the second.

func ReadHeld

func ReadHeld(path string) []HeldPush

ReadHeld parses a hold file.

func (HeldPush) Delete

func (h HeldPush) Delete() bool

Delete reports whether the push deletes Dst.

type Hold

type Hold struct {
	// contains filtered or unexported fields
}

Hold holds the pushes of one task's agent. Create it before the agent runs, hand Env to the agent's process, and Close it when the task is decided.

func NewHold

func NewHold(helperArgv []string) (*Hold, error)

NewHold prepares a hold whose helper runs helperArgv (the cloop binary and HelperSubcommand) with git's arguments appended.

func (*Hold) Close

func (h *Hold) Close()

Close removes the hold's files. The agent's environment still names them, so a push after Close fails rather than going out unreviewed.

func (*Hold) Env

func (h *Hold) Env() []string

Env returns the variables that make the agent's pushes land in this hold. They go on top of the agent's inherited environment: the git configuration entries are numbered after any the environment already carries, because GIT_CONFIG_COUNT is one variable for the whole environment and a sandbox's workspace delivery uses it too.

func (*Hold) Pushes

func (h *Hold) Pushes() []HeldPush

Pushes returns what the hold recorded, oldest first, bounded and with malformed lines skipped: the file is in reach of the agent.

type Input

type Input struct {
	Goal         string
	Conventions  string // the project's instructions to its agents
	TaskID       int
	TaskTitle    string
	TaskDesc     string
	AgentReport  string // the agent's final message
	Changes      *Changes
	Instructions string // the operator's extra review criteria
	// Round is 1 for the first review; later rounds follow a fix turn and
	// carry what the previous round asked for.
	Round         int
	PriorFindings []pm.ReviewFinding
	PriorSummary  string
	// CanReadFiles tells the reviewer it may open files for context, which
	// is true for a harness reviewer given read-only tools.
	CanReadFiles bool
}

Input is everything a review round is shown.

type RepoChanges

type RepoChanges struct {
	// Dir is the repository's top-level directory; Rel the same relative to
	// the project ("." for the project's own repository).
	Dir string
	Rel string
	// Base is the commit the diff starts from ("" means the empty tree: the
	// whole repository is new). Head is HEAD when the changes were collected
	// ("" before the first commit).
	Base string
	Head string
	// Branch is the checked-out branch ("" when HEAD is detached).
	Branch string
	// Tree is the tree `git add -A` would commit from the working state. It
	// is what a merge performed after the review must match.
	Tree string
	// Commits lists Base..Head, newest first ("<short sha> <subject>");
	// CommitCount is the full count.
	Commits     []string
	CommitCount int
	// Files, Insertions and Deletions summarise the diff, untracked files
	// included.
	Files      int
	Insertions int
	Deletions  int
	// Uncommitted reports changes in the working tree that are not in Head.
	Uncommitted bool
	// CloopFiles lists changed paths under .cloop/, which are cloop's own
	// bookkeeping and shown by name only.
	CloopFiles []string
	// Diff is the unified diff of the working tree against Base, with new
	// untracked files rendered as additions. Truncated reports it was cut.
	Diff      string
	Truncated bool
	// Held are the pushes the agent attempted from this repository.
	Held []HeldPush
}

RepoChanges is one repository's contribution to a review.

type Reviewer

type Reviewer struct {
	Provider provider.Provider
	// ProviderName and Model name it for the record. Model is passed to
	// the provider; empty means the provider's default.
	ProviderName string
	Model        string
	Effort       string
	Timeout      time.Duration
}

Reviewer is a project's configured review model.

func (*Reviewer) Label

func (r *Reviewer) Label() string

Label names the reviewer for people: its model when set, else its provider.

func (*Reviewer) Review

func (r *Reviewer) Review(ctx context.Context, workDir string, in Input) Round

Review runs one review round in workDir.

A harness provider is given read-only tools (provider.Options.ReadOnly), so the reviewer can open files for context and cannot change the work it is judging. An answer with no verdict in it earns one more request for the format — in the same conversation where the provider keeps one — and a second such answer is an error, which the gate treats as no verdict.

type Round

type Round struct {
	// Verdict is nil when Err is set.
	Verdict *Verdict
	Err     error
	// Tokens the reviewer spent, and the model the provider reports.
	InputTokens  int
	OutputTokens int
	Model        string
}

Round is the outcome of one review call.

type Snapshot

type Snapshot struct {
	// Root is the directory the task works in.
	Root string
	// Heads maps each repository's top-level directory to its HEAD commit
	// ("" for a repository with no commits yet).
	Heads map[string]string
}

Snapshot records the repositories under a project before a task runs, so a review can tell what the task changed from what was already there.

func TakeSnapshot

func TakeSnapshot(ctx context.Context, root string) *Snapshot

TakeSnapshot records the repositories under root and their HEADs.

func (*Snapshot) Head

func (s *Snapshot) Head(dir string) (string, bool)

Head returns the HEAD the repository at dir had when the snapshot was taken, and whether the snapshot saw that repository at all.

type Verdict

type Verdict struct {
	Approved bool
	Summary  string
	Findings []pm.ReviewFinding
}

Verdict is one review round's decision.

func ParseVerdict

func ParseVerdict(output string) (*Verdict, error)

ParseVerdict reads a reviewer's answer.

It fails closed. An answer that states no verdict is an error, never an approval; and an "approve" that lists a blocker or major finding is read as a request for changes, because the prompt defines approval as having none — a reviewer contradicting itself does not get the benefit of the doubt.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL