Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Acceptance Gates

A gate is a command that must pass before you are asked to review. cargo test, cargo clippy, npm test, a lint, a formatter check: whatever your project already uses to decide that a change is acceptable. Gates turn "the model says it is done" into "the model says it is done and the tests agree".

Gates, including final publication gates, participate in execution locks. Array commands match exec(program) args(...); shell strings match shell(<expanded command>). Matching happens after {files} expansion. Skipped gates acquire nothing. Gate deadlines exclude contention; ownership covers execution and supervised cleanup. After a gate passes, resources shared with the next executed gate remain held, preventing other sessions from interleaving operations on those resources. Skipped gates do not break retention. Failure or the end of the run releases ownership; if additional resources are busy, the transition releases the held set and retries rather than risking a deadlock.

Configuring a gate

Gates live in .heph/config.toml in the project (or the user config, for gates you want everywhere):

[[gates]]
name = "test"
paths = ["**/*.rs", "Cargo.toml"]
command = ["cargo", "test"]
retry_limit = 2

[[gates]]
name = "fmt"
paths = ["**/*.rs"]
command = ["cargo", "fmt", "--check"]

[[gates]]
name = "docs"
paths = ["**/*.md"]
command = ["mdformat", "--check", "{files}"]
KeyMeaning
nameShown in the status line and to the model.
pathsGlobs. The gate runs when the turn changed a matching path. Empty or absent: every turn.
commandShell string or direct-execution array, run in the worktree. Exit 0 passes. {files}: see below.
retry_limitHow many failures the model may fix on its own before you are shown the review anyway. Default 3.

An array's first string names the executable; the rest are literal arguments. command = ["cargo", "test"] runs directly and matches a lock rule such as exec(cargo) args(*). A string still runs through sh -c; use it for shell features, for example command = "cargo test && echo passed". Stdout and stderr are captured in both forms, so a direct-exec gate needs no redirection.

Arrays cannot be empty, start with an empty executable, or contain NUL bytes. {files} must be a whole argument, never part of the executable or another argument. See Gate configuration for the exact fields.

Gates run in config order. Project gates come after user gates. The first failure stops the run; later gates wait for the next attempt.

The globs use path semantics: * stops at /, ** does not, so **/*.rs means any Rust file at any depth.

Code, Design, and Mixed Publications

A gate may use modes = ["code"], ["design"], or both; omitted modes means unrestricted. Mode and path restrictions must both match. Empty lists and unsupported names are errors. Quick and recorded review turns are code work. File extensions do not decide the mode: code work on Markdown remains code. Design gates may run test suites if that suits your project.

[[gates]]
name = "tests"
modes = ["code"]
command = "cargo test"

[[gates]]
name = "design-check"
modes = ["design"]
command = "check-designs"

[[gates]]
name = "markdown"
paths = ["**/*.md"]
command = "mdformat --check {files}"

check-designs is your own project command, not a heph command. A design turn runs its design checks and matching shared checks. Retries and partial acceptance use that turn's original mode, even if the session mode changes. Resumed reviews retain their stored class and do not repeat completed gate passes. Manual /review keeps its existing gate-timing exceptions; newly reviewed unattributed work is code work.

At /commit, modes come from accepted unpublished work that remains in the candidate. Accept code, switch to design, then publish: code checks still run. Accept both modes before publishing: both sets run, each eligible entry once, against the combined candidate's matching files. Successful publication covers only that candidate; fresh design-only work afterward does not inherit code mode. Failed gates, hooks, cancelled messages, and refused branch updates retain pending provenance across restart. Navigation and forks follow snapshot content; restored code cannot become design-only work. A fully reverted separate code path no longer forces code checks; same-path rewrites retain both modes conservatively. Incomplete legacy provenance selects both modes with a diagnostic. Git hooks are unchanged.

Checking only the changed files

A formatter or linter usually needs to check only the files the turn touched. Write {files} in the command where they go, as the docs gate above does. Heph replaces it with the changed paths that match the gate's paths. In an array, a whole {files} argument expands to separate literal filename arguments. In a shell string, each filename is quoted for the shell. In either case, a name with spaces stays one argument. A gate without paths gets every changed path. Deleted files are left out, since there is nothing to check.

Every gate also gets the list in the HEPH_GATE_FILES environment variable, one path per line, for a script that would rather read it than take arguments.

If every file a {files} gate matched was deleted, the gate is skipped with a status line saying so. Run with no arguments, most formatters would check the whole tree. A gate that reads only HEPH_GATE_FILES is not skipped; it runs with the variable empty.

The list matches what is being checked: the turn's changes before review, what you accepted when the gates run again after a rejection, and everything a /commit would publish.

What the model sees

When a gate fails, the model gets its output and a short instruction:

The acceptance gate `test` failed. First determine whether the failure is
related to your changes, using the diagnostics and relevant code. If related,
fix it and rerun the check. If unrelated or uncertain, explain the evidence
and what is blocking verification. Do not change unrelated code, weaken or
bypass the gate, or claim it passed.

running 6 tests
test tests::divides ... FAILED
...

The model first investigates relatedness. It repairs failures caused by its changes; for unrelated or uncertain failures, it explains the evidence and the verification blocker instead of changing unrelated code or weakening the gate. This is an instruction, not a passing verdict: checks and retry limits still apply, and a failed publication gate still blocks committing.

If a gate fails during /commit, nothing is committed and heph automatically starts a new repair turn with that same feedback. Normal permissions, loop detection, gates, and review apply. After accepting the repair, run /commit again; heph does not retry publication automatically. Cancelling publication does not start a repair turn.

During a turn, the model edits, stops, and the gates run again from the first. Each gate counts its own failures; when a gate has failed more than its retry_limit, heph stops asking the model and opens the review with the failure attached:

Review: 01a0c63 on main, round 1 -- 1 file, 3 hunks
(gate failure, shown as it stands)
test:
running 6 tests
test tests::divides ... FAILED

You decide what to do with a change that does not pass: accept it and fix it yourself, reject the hunks that broke it with a comment, or reject all.

Gates and review

Gates run before review, on everything the model changed. If you then accept only some hunks, they run again on what you accepted, because that is a state the model never tested. A failure there goes back to the model as a consequence of your rejection, naming what you rejected, and nothing is applied until a round passes. See Rejections, Comments, and the Next Round.

Gates do not run on an interrupted turn before its review, since the work is unfinished by definition. They do run on whatever you accept from it before it lands.

Where gates run

In the worktree, as the model's exec and shell commands do, with the same environment: CARGO_TARGET_DIR points at the project's shared target directory, so a test gate reuses the dependencies you have already built and adds only the changed crates. A gate's output streams into the conversation under the -- running acceptance gates line as it arrives, so a slow suite is visible while it runs. Each gate may run for up to thirty minutes.

Gates run with no permission prompt. They are your commands, from your config file, and configuring them is the approval.

Plan turns

A plan turn makes no changes, so no gate is due. If a gate somehow fails on one, the model is not asked to fix it, since it has no tools to fix anything with.

Choosing gates

A gate is worth having when it is fast enough to run after every turn and strict enough that you would not accept a change that fails it. cargo check is a good first gate on a large crate where cargo test is slow; cargo test alone is right for most. A formatter check is worth adding once you have seen the model produce something you would have reformatted.

A gate that fails for reasons unrelated to the change, a flaky test, a network call, teaches the model to work around it. Keep gates deterministic.