Skip to content

fix(work-driver): an unreadable diff makes six-pass review self-approve instead of halting #384

Description

@randomm

Context

Raised by an operator asking whether the six-prong review had been getting skipped. Measured across every .pi/work-state/*.json in this repo: it had not — every cycle that reached lens-review ran lens-review×6, and no cycle recorded a lens-skipped-empty-diff event. Cycles where the review appears absent (#277, #279, #305, #308) aborted or handed off before reaching the step.

But the question exposed a real hazard, and it is the same class as #380's ci-status: substring: a gate whose "no signal" answer is approval.

runLens (work-driver-lens.ts:98-104) treats an empty diff as approved:

if (!diff.trim()) {
  next = appendEvent(next,
    { kind: "lens-skipped-empty-diff", ... },
    { kind: "lens-approved", ... });   // <-- approves
  return next;
}

And the diff it tests comes from fetchIntegratedDiff (work-driver-diff.ts:131-149), which swallows every error and returns "":

} catch (err) {
  trace(`work-driver: fetchIntegratedDiff(${branchName}) failed: ...`);
  return "";
}

So a transient git failure, a stale origin/<branch> ref, a maxBuffer overrun on a large diff, or an unset branchName all produce the same value as "there is genuinely nothing to review" — and that value means approve and proceed to merge. Under always-worktree (#287) the fallback path (fetchMergedDiff from a worktree detached at baseSha) is also empty, so there is no second opinion.

This has bitten twice already from the other direction: pre-PR11 the guard fired on every successful cycle (34ms skip → code merged unreviewed), and #287's comment records the same failure being re-introduced when worktrees went detached. Both were fixed by changing which diff is read. The structural problem — that an unreadable diff is indistinguishable from an empty one, and both approve — was never fixed.

What to build

A. Distinguish "empty" from "unreadable." fetchIntegratedDiff must report failure rather than returning "". A discriminated result ({ok: true, diff} / {ok: false, reason}) or a thrown error the caller handles — either is fine; what matters is that the caller can tell the two apart.

B. An unreadable diff must NOT approve. Emit a cap-hit (lens-diff-unreadable or similar) routing to handoff, with the git error in the message. Halting on an unreadable diff is cheap; merging unreviewed code is not.

C. A genuinely empty diff should still be justified. Confirm it positively — e.g. git rev-list --count origin/<base>..origin/<branch> is 0 — rather than inferring it from the absence of output. Same shape as #380's evidence gate: prove the green, do not assume it.

D. Make the skip visible. lens-skipped-empty-diff currently only lands in the event log. When a cycle merges without a six-pass review, that fact belongs in the PR body and the merged event, not only in a JSON file nobody opens.

Acceptance criteria

  • A fixture where the git call FAILS produces a cap-hit → handoff, and zero lens-approved events.
  • A fixture where the branch genuinely has no commits ahead of base still approves, and records the positive check that established it.
  • A fixture where the diff is large enough to trip maxBuffer does not silently approve.
  • Canary: reverting the fix makes the first two fail (the current code passes them only by accident of returning "").
  • A cycle that skipped the review says so in the PR body.
  • Quality gate green per AGENTS.md §1; docs updated.

Out of scope

Changing which diff is read (that is settled by #287) or the six lenses themselves.

This work must ship as its own separate PR, independent of any other open issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions