Skip to content

fix(review): negotiated START freezes an empty candidate on a clean post-apply worktree #2172

Description

@aparragithub

Pre-flight Checklist

  • I have searched existing issues and this is not a duplicate
  • I understand that PRs will be rejected if the linked issue does not have 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-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

  1. Create a feature branch with three commits ahead of origin/main, and leave the worktree clean (git status reports nothing pending).
  2. 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
    
  3. Observe the frozen authority: projection: workspace, changed_files: 0, changed_lines: 0, paths: [], and equal base/candidate trees.
  4. Finalize the zero-lens review and validate pre-commit. Observe allow.
  5. 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>.
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions