Rejections, Comments, and the Next Round
What you say in review is what the model hears next. This chapter is about the conversation that a rejection starts, so you can steer it.
What the model is told
After a review with any rejection or comment, the model's next round begins with a message heph writes from your verdict:
Your proposed changes were reviewed. Here is the result:
file: src/auth.rs
hunk 1 (lines 23-31): REJECTED
your rationale: "Read the JWT secret from the environment so the key
never lands in the binary."
user: "put it in the config file instead, not the environment"
hunk 2 (lines 58-70): ACCEPTED
file: src/main.rs
hunk 1 (lines 10-25): ACCEPTED
Address the comments. The rejected hunks are constraints: do not re-propose
them in any form. The worktree already holds only what was accepted.
Each rejected hunk carries the model's own rationale back to it, and your comment beneath. The model learns which reasoning was refused, not only which lines, and that is what lets it converge: a rejection that answers a stated reason usually takes one more round where a bare "no" takes several.
Nothing in the message is a paraphrase. Your comment is quoted exactly; the rationale is the model's own sentence.
Constraints
Every rejected hunk becomes a constraint for the rest of the turn. Each is listed once, with your comment and the rejected text, at the end of the review message that sends the model back to work; that message stays in front of the model for every later round of the turn:
The user rejected the following changes in this turn. Do not re-propose them
in any form.
src/auth.rs, round 1 -- "put it in the config file instead, not the
environment"
+ let secret = std::env::var("JWT_SECRET")
+ .expect("JWT_SECRET must be set");
Long hunks are elided in the middle. A constraint lasts for the turn and dies with it: the next prompt may ask for the same thing deliberately.
Re-proposals
Before each review, heph compares every proposed hunk with the turn's rejected
ones: same file, whitespace ignored, by the share of the rejected hunk's lines
that reappear. Above the threshold ([review] reproposal_threshold, default
0.8) the hunk is a re-proposal. It is rejected before you see it, listed in
the brief as [re-proposed (round N); rejected] and left out of the hunks you
are asked about, and the model is told, in the constraints, "you re-proposed a
rejected change. Do not." You never reject the same thing twice.
One exception, because a comment asks for a variant: a hunk rejected with a comment does not mark re-proposals in the round that answers it. "No doc comment" on a five-line function leaves four lines standing, which would otherwise look like the same hunk. You see the model's answer to your comment and judge it; from the round after, it is a constraint like any other. A rejection without a comment is a constraint at once.
After two re-proposals in one turn, heph stops asking the model: whatever you accepted is applied, the turn ends, and the status line says so. A model that will not take no for an answer does not get a third round to spend.
Rounds after a review
The next round runs in the worktree as it stands: your accepted hunks in place, the rejected ones gone. When the model stops, the gates run again on everything you kept plus what it just changed, and a new review opens showing only the new change, as round 2 of the same turn. The header says which round you are on.
This repeats until a review ends with nothing rejected and no comments, or you
press q, or the re-proposal floor is reached, or you interrupt.
When the model gets no round
Some reviews end the turn instead of starting a round: the turn was interrupted,
you pressed q on the brief, the review was a /review, you stopped the
built-in review with Esc partway, or the floor was reached. The model still
needs to know what happened, or it would carry on from its last message
believing its rejected code is in place. So the same feedback is written into
the turn for it to read next time, ending not with "address the comments" but
with what happens instead:
The turn ends here. The worktree holds only what was accepted; the rejected
changes are not in place, so do not build on them. If the user wants them
done differently, their next prompt will say so.
A review stopped partway lists the hunks you did not reach as UNDECIDED and
says the review is not finished; a file you edited with e shows every hunk as
EDITED BY THE USER, since your saved file decided them all.
q on the brief is the one refusal that never gets a round: a wholesale "no"
without words gives the model nothing to act on but a guess, and a round spent
guessing is a round wasted. Say what you wanted in your next prompt. In the
middle of the built-in review, q refuses the hunks you have not reached and
finishes the review, which then follows the ordinary rules.
Gates after a partial acceptance
Accepting some hunks and rejecting others produces a state the gates have not seen: the tests passed with all of the model's changes, not with half. So before anything is applied, the gates run on the accepted state. A failure goes back to the model as a consequence of the rejection, with the gate's output and the constraints, and nothing is written to your tree until a later round passes. If the turn was interrupted, or the model has run out of rounds, the failure is shown to you with the review instead.
Hunks set aside at apply
You accepted a hunk, but the region it changes has changed in your tree since the turn began: you edited the file by hand while the model worked. Your text stands. The hunk is set aside, the file's other hunks still land, and the model's next round is told:
file: src/lib.rs
SET ASIDE AT APPLY: this region changed in the user's tree after the turn
began. The current text is below. Redo your change against it, or explain
why it no longer applies.
followed by the current text of the file. Heph first writes that text into the worktree and commits it on the chain, so the model's next edit is against what is really there. No conflict marker is ever written to your files.