Pre-flight Checklist
📝 Bug Description
On a clean feature branch whose SDD implementation is already committed, the negotiated review loop freezes a candidate that reviews nothing, approves it, and only fails three gates later with a message that describes the wrong problem.
review status --contract gentle-ai.review-integration/v2 --next-transition proposes review.start with projection: workspace. Because the worktree is clean, START freezes changed_files: 0, changed_lines: 0, paths: [], and base_tree == candidate_tree. The zero-lens low-risk review finalizes as approved and pre-commit returns allow. Delivery then fails at pre-push, and no base selector can repair it, because the reviewed scope is empty rather than merely mis-based.
This is a distinct mechanism from #2090 (closed by #2169), where the freeze was correct and the emptiness appeared later at validation after a strict-subset correction and a recovered successor. Here the freeze itself is born empty, with no correction and no recovery involved. It is also the START-side sibling of #1203, which fixed the post-apply variant of "clean worktree after the approved candidate was committed".
Root trigger: internal/cli/review_next_transition.go proposes review.start with TargetCurrentChanges whenever target applicability is unrelated, with no clean-workspace check. This is the standard STATUS-driven loop, not a misuse.
🔄 Steps to Reproduce
- Create a feature branch with three commits ahead of
origin/main, and leave the worktree clean (git status reports nothing pending).
- Run negotiated STATUS and execute exactly the
review.start transition it returns:
gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --next-transition
- Observe the frozen authority:
projection: workspace, changed_files: 0, changed_lines: 0, paths: [], and equal base/candidate trees.
- Finalize the zero-lens review and validate
pre-commit. Observe allow.
- Validate delivery without a base selector:
gentle-ai review validate --cwd <repo> --lineage <lineage-id> --gate pre-push
Observe invalidated: configured upstream is missing or ambiguous; pass --base-ref <remote>/<branch>.
- Retry with the explicit base:
gentle-ai review validate --cwd <repo> --lineage <lineage-id> --gate pre-push --base-ref origin/main
Observe invalidated: compact gate inputs cannot be derived: reviewed delivery is not exactly one commit from its reviewed base.
✅ Expected Behavior
For a post-apply candidate whose implementation is already committed, negotiated START should either freeze the committed delivery range against the intended base, or refuse before issuing an approval that no delivery gate can consume — naming --base-ref <commit> as the way to review committed work.
The repository already contains exactly that guidance as reviewStartEmptyCandidateHint in internal/cli/review_facade.go:
the candidate has no pending changes; already-committed work can be reviewed by rerunning review start with --base-ref <commit> naming the base to compare against
It is emitted only on the unnegotiated path, so the negotiated contract path never receives it.
❌ Actual Behavior
Negotiated START approves an empty workspace projection. pre-commit allows it. Delivery validation then evaluates the non-empty committed range against a receipt that covers nothing.
The emitted error is also misleading. reviewedDeliveryBase in internal/reviewtransaction/gate.go scans the publication range for the commit whose tree equals the receipt's delivery base tree. With base_tree == candidate_tree == HEAD's tree, the only match is HEAD itself, so the derived range holds zero commits — not "more than one", which is what reviewed delivery is not exactly one commit from its reviewed base implies.
A precise diagnostic for this shape already exists in internal/reviewtransaction/compact_gate.go (pre-push current-changes receipt requires a delivered tree change), but it is unreachable here: target derivation fails first and returns the opaque derivation error, and the retry beside it only applies when a recovery chain exists.
The explicit-maintainer action can supply a base selector, but cannot repair an empty reviewed scope.
💡 Logs / Error Output
$ gentle-ai review validate --cwd <repo> --lineage <lineage-id> --gate pre-push --base-ref origin/main
{
"schema": "gentle-ai.review-gate-result/v1",
"result": "invalidated",
"allowed": false,
"action": "explicit-maintainer-action",
"reason": "compact gate inputs cannot be derived: reviewed delivery is not exactly one commit from its reviewed base",
"context": {
"gate": "pre-push",
"generation": 1,
"base_tree": "<tree-sha>",
"candidate_tree": "<tree-sha>",
"paths_digest": "<digest of the empty path set>"
}
}
candidate_tree is byte-identical to base_tree. Frozen scope on the approved authority: changed_files: 0, changed_lines: 0, paths: [].
Worktree and index remained clean throughout. No push, PR, merge, history rewrite, recovery, additional review, or authority mutation was attempted, and no delivery side effect occurred.
Additional Context
Verified against a branch with three commits, which matters for the fix shape: auto-deriving a base such as HEAD~1 would have silently frozen one third of the work as if it had succeeded. The correct base was origin/main, i.e. HEAD~3. Choosing the base is the reporter's call, so the tool should state that a base is required rather than guess one.
Related: #2090 (closed by #2169) shares the empty-receipt symptom but not the cause. #1203 fixed the post-apply variant of the same committed-clean-worktree family.
Pre-flight Checklist
status:approved📝 Bug Description
On a clean feature branch whose SDD implementation is already committed, the negotiated review loop freezes a candidate that reviews nothing, approves it, and only fails three gates later with a message that describes the wrong problem.
review status --contract gentle-ai.review-integration/v2 --next-transitionproposesreview.startwithprojection: workspace. Because the worktree is clean, START freezeschanged_files: 0,changed_lines: 0,paths: [], andbase_tree == candidate_tree. The zero-lens low-risk review finalizes asapprovedandpre-commitreturnsallow. Delivery then fails atpre-push, and no base selector can repair it, because the reviewed scope is empty rather than merely mis-based.This is a distinct mechanism from #2090 (closed by #2169), where the freeze was correct and the emptiness appeared later at validation after a strict-subset correction and a recovered successor. Here the freeze itself is born empty, with no correction and no recovery involved. It is also the START-side sibling of #1203, which fixed the
post-applyvariant of "clean worktree after the approved candidate was committed".Root trigger:
internal/cli/review_next_transition.goproposesreview.startwithTargetCurrentChangeswhenever target applicability isunrelated, with no clean-workspace check. This is the standard STATUS-driven loop, not a misuse.🔄 Steps to Reproduce
origin/main, and leave the worktree clean (git statusreports nothing pending).review.starttransition it returns:projection: workspace,changed_files: 0,changed_lines: 0,paths: [], and equal base/candidate trees.pre-commit. Observeallow.invalidated:configured upstream is missing or ambiguous; pass --base-ref <remote>/<branch>.invalidated:compact gate inputs cannot be derived: reviewed delivery is not exactly one commit from its reviewed base.✅ Expected Behavior
For a post-apply candidate whose implementation is already committed, negotiated START should either freeze the committed delivery range against the intended base, or refuse before issuing an approval that no delivery gate can consume — naming
--base-ref <commit>as the way to review committed work.The repository already contains exactly that guidance as
reviewStartEmptyCandidateHintininternal/cli/review_facade.go:It is emitted only on the unnegotiated path, so the negotiated contract path never receives it.
❌ Actual Behavior
Negotiated START approves an empty workspace projection.
pre-commitallows it. Delivery validation then evaluates the non-empty committed range against a receipt that covers nothing.The emitted error is also misleading.
reviewedDeliveryBaseininternal/reviewtransaction/gate.goscans the publication range for the commit whose tree equals the receipt's delivery base tree. Withbase_tree == candidate_tree == HEAD's tree, the only match is HEAD itself, so the derived range holds zero commits — not "more than one", which is whatreviewed delivery is not exactly one commit from its reviewed baseimplies.A precise diagnostic for this shape already exists in
internal/reviewtransaction/compact_gate.go(pre-push current-changes receipt requires a delivered tree change), but it is unreachable here: target derivation fails first and returns the opaque derivation error, and the retry beside it only applies when a recovery chain exists.The explicit-maintainer action can supply a base selector, but cannot repair an empty reviewed scope.
💡 Logs / Error Output
candidate_treeis byte-identical tobase_tree. Frozen scope on the approved authority:changed_files: 0,changed_lines: 0,paths: [].Worktree and index remained clean throughout. No push, PR, merge, history rewrite, recovery, additional review, or authority mutation was attempted, and no delivery side effect occurred.
Additional Context
Verified against a branch with three commits, which matters for the fix shape: auto-deriving a base such as
HEAD~1would have silently frozen one third of the work as if it had succeeded. The correct base wasorigin/main, i.e.HEAD~3. Choosing the base is the reporter's call, so the tool should state that a base is required rather than guess one.Related: #2090 (closed by #2169) shares the empty-receipt symptom but not the cause. #1203 fixed the
post-applyvariant of the same committed-clean-worktree family.