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}"]
| Key | Meaning |
|---|---|
name | Shown in the status line and to the model. |
paths | Globs. The gate runs when the turn changed a matching path. Empty or absent: every turn. |
command | Shell string or direct-execution array, run in the worktree. Exit 0 passes. {files}: see below. |
retry_limit | How 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.