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
- Variables
- func BuildPrompt(in Input) string
- func FixPrompt(reviewer string, v *Verdict, round, maxRounds int, holding bool, resumed bool, ...) string
- func PromptSection(mode string, holding bool) string
- func Publish(ctx context.Context, reviewed *Changes, pushes []HeldPush) []pm.ReviewPublish
- func ServeRemoteHelper(ctx context.Context, args []string, holdFile string, in io.Reader, ...) error
- func Toplevel(ctx context.Context, dir string) string
- func TreeOf(ctx context.Context, dir, rev string) string
- func Withhold(root string, pushes []HeldPush, why string) []pm.ReviewPublish
- type Changes
- type HeldPush
- type Hold
- type Input
- type RepoChanges
- type Reviewer
- type Round
- type Snapshot
- type Verdict
Constants ¶
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.
const HelperSubcommand = "review-gate-remote-helper"
HelperSubcommand is the hidden cloop subcommand that serves the transport.
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>".
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 ¶
var ErrNoVerdict = errors.New("the reviewer's answer contained no verdict")
ErrNoVerdict is returned when a reviewer's answer contains no verdict.
Functions ¶
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 ¶
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 ¶
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 ¶
Toplevel returns the top-level directory of the repository containing dir, or "" when dir is not in one.
Types ¶
type Changes ¶
type Changes struct {
Root string
Repos []RepoChanges
}
Changes is what a task changed, per repository.
func Collect ¶
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) 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.
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 ¶
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 ¶
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.
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 ¶
Label names the reviewer for people: its model when set, else its provider.
func (*Reviewer) Review ¶
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 ¶
TakeSnapshot records the repositories under root and their HEADs.
type Verdict ¶
type Verdict struct {
Approved bool
Summary string
Findings []pm.ReviewFinding
}
Verdict is one review round's decision.
func ParseVerdict ¶
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.