Skip to content

feat: Commit Review, fix-with-agent + iterations/coverage + review trailer (v3.7.0 part 2) - #160

Closed
devlint wants to merge 12 commits into
feat/v3.7-commit-reviewfrom
feat/v3.7-commit-review-pr2
Closed

feat: Commit Review, fix-with-agent + iterations/coverage + review trailer (v3.7.0 part 2)#160
devlint wants to merge 12 commits into
feat/v3.7-commit-reviewfrom
feat/v3.7-commit-review-pr2

Conversation

@devlint

@devlint devlint commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Second slice of ROADMAP.md's v3.7.0 "Commit Review" feature, stacked on #159 (part 1: staged-diff review engine + issue navigation). This PR adds:

  • Fix with agent — pipe the current review findings to an agent (Claude Code / opencode / Codex) via a terminal tab. The prompt is typed into the agent's PTY without pressing Enter (no forced submit, no boot-race), and re-review re-arms on the next staged-set change. Scratch-worktree handoff was deliberately dropped from this PR: manual QA found that a brand-new, never-trusted worktree can trigger a CLI's first-run "trust this directory?" onboarding screen instead of its normal input, and piped newlines get interpreted as menu navigation there instead of text (this genuinely misfired twice during testing, once completing an unintended brew upgrade). "Fix with agent" in this PR only ever targets the current, already-trusted repo. Scratch-worktree support for this feature is left as a follow-up once there's a real fix for the onboarding-screen risk (e.g. pre-trusting the directory before launching the agent).
  • Iterations & coverage — tracks review→fix→review cycles (iter:N) and the share of the staged diff already reviewed (coverage:X%), content-hash based so line-number shifts from a fix don't destroy the count. The review cycle is bound to HEAD, so an amend or an out-of-app commit correctly starts a fresh cycle instead of reusing stale state. Coverage is recomputed against the full current staged diff immediately before the commit trailer is written, so it can't lie because of watcher granularity or a review's file/byte truncation cap.
  • Review / Vouch / Skip — an explicit three-state decision at commit time, non-blocking (same UX contract as the v3.5.0 secrets scanner), recorded as GitWand-Review: ran|vouched|skipped (iter:N, coverage:X%) via the existing commit-trailers support. The parenthetical is omitted for a plain skipped with no review activity.

This PR went through three rounds of implementation + adversarial verification. Round 1 found a blocking binary-file corruption (a literal NUL byte in a source file) plus real logic gaps in the coverage/iteration accounting and the PTY safety story. Round 2's fix for the coverage accounting was itself found to be incomplete (still wrong in the most common edit-and-restage-the-same-file workflow, and after a truncated review) and was fixed and re-verified in round 3. A final low-severity finding (an optimistic 100% fallback when the feature is toggled off mid-cycle) was fixed directly after the third pass confirmed everything else.

Test plan

  • pnpm --filter @gitwand/core run test -- --run — 1056/1056 passing (unchanged)
  • cd apps/desktop && pnpm test -- --run — 831/831 passing (103 files, +101 tests vs PR1's baseline: fix-with-agent prompt building, iteration/coverage state incl. HEAD-cycle binding and snapshot pruning, the decision modal, and multiple race-condition regression tests)
  • cd apps/desktop && pnpm build — clean vue-tsc --noEmit + vite build
  • Manual QA of the agent PTY handoff against real claude and codex sessions (via the dev-server's PTY backend) confirmed the no-auto-submit design holds once an agent is at its normal ready input
  • The codex/opencode newline-handling assumption behind "type without pressing Enter" is verified for claude but not fully for codex/opencode — a bracketed-paste wrap was considered as a more robust alternative to the current boot-timing wait but not implemented (further manual QA attempts risked repeating the onboarding-screen misfire); left as a documented follow-up, not a blocker, consistent with dropping scratch-worktree support in this PR

Laurent Guitton added 12 commits August 17, 2026 10:47
…ch worktree

Task 3 of v3.7.0: adds buildReviewFixPrompt (pure, no secrets, PTY-safe), a
Fix with agent control in CommitReviewModal (tool picker + scratch worktree
checkbox), and useCommitReview.armReReview/onStagedSetChanged, the real
staged-set-watcher trigger for the one-shot re-review after a fix handoff
(decision D7: the prompt is typed into the agent PTY without pressing Enter).
Factors confirmNewAiTask's scratch-worktree sequence into a shared helper.
Un-hides the previously dangling commitReviewAutoReReview setting now that
it has a real consumer.

disabled
Task 4 of v3.7.0: adds commitReviewState.ts, a pure, localStorage-persisted
store (pattern: usePrCache.ts) tracking review->fix->review iterations and
content-hash based coverage of the staged diff's added lines, so a fix that
shifts line numbers never destroys coverage. Wires iterations/coverage into
useCommitReview (recorded only for a non-aborted run, guarding against the
same stale-run race already covered for running/progress), refreshes
iterations from persisted state on repo switch, and clears state after a
successful commit (a new commit starts a new review cycle).

disabled
…ed/skipped decision

Task 5 of v3.7.0: adds buildReviewTrailer, the pure commit-gate helpers
(resolveCommitReviewGate, effectiveReviewDecision, appendReviewTrailer) and
CommitReviewDecisionModal.vue, wired into App.vue's handleCommitRequest via
a shared proceedToCommit path (also used by the secrets "Commit anyway"
route, so both funnel through the same gate). Same non-blocking UX contract
as the v3.5.0 secrets scanner: cancelling the decision modal cancels the
commit rather than silently recording "skipped" (decision D8). "Review now"
leaves the commit pending; a later commit click with iterations > 0 proceeds
without re-prompting and records "ran". A successful commit clears the
review-state cycle.

disabled
…flakiness

The real-timer version of the debounce/one-shot re-review test had only a
few ms of margin between its waits and the debounce window, and failed once
under load during full-suite verification. Switches it to fake timers
(pattern: useSecretsScanner.test.ts), scoped to just this one test.

disabled
….ts binary

hashLineKey's template literal had a literal, embedded NUL byte instead of
the \0 escape sequence, which made the whole file register as binary to
git (git diff showed "Binary files differ", GitHub would render it as
"Binary file not shown", and git blame/merge/diff were broken on it going
forward). Replaces the raw byte with the \0 escape sequence in source, so
the runtime hash values are byte-identical (verified: every existing
hashLineKey test still passes unmodified) while the file itself is plain
UTF-8 text. Adds a regression guard that reads the source file's own bytes
and fails if a raw NUL character ever reappears.

disabled
…onest between reviews

Two related correctness fixes to the iterations/coverage tracking:

1. Coverage no longer asserts 100% right after a plain staged-set change.
   onStagedSetChanged now recomputes it against the CURRENT staged diff (a
   plain git diff fetch, no LLM call), guarded by a generation counter so a
   stale in-flight refresh can never clobber a newer one or a real run().
   Previously a brand-new unreviewed file staged after a completed review
   kept showing coverage:100% until the next explicit review click.

2. iterations is now bound to the repo's HEAD commit, not just the review
   count. run() resolves and stamps HEAD via a single git rev-parse HEAD
   call per run, and reconcileIterationsForHead (awaited by the commit
   gate before it ever reads iterations) resets the count to 0 when HEAD
   moved since the last recorded review. Without this, a commit made
   outside the app (amend, terminal commit, any external tool) left a
   stale iterations count that let the gate skip the decision modal and
   write a "ran" trailer for a review that never happened against what
   was actually being committed. Also resets iterations when snapshot
   pruning empties a repo's snapshot list entirely (aged-out evidence).

disabled
… now, harden the agent handoff

- proceedToCommit awaits commitReview.reconcileIterationsForHead before
  resolveCommitReviewGate ever reads iterations, so a commit made outside
  the app since the last review is caught before the gate decision (see
  the companion commitReviewState/useCommitReview fix).
- onCommitReviewDecisionReviewNow ("Review now" in the decision modal) now
  reuses onReviewStagedClicked directly instead of a weaker duplicate:
  it only opens the findings modal when a review actually completed
  without error, and surfaces "no AI provider configured" via repoError
  when the run never even attempted. Previously it always popped an empty
  "No findings" modal regardless of outcome.
- onCommitReviewFixWithAgent adds a short readiness wait before typing the
  prompt into a freshly spawned agent PTY, and surfaces scratch-worktree
  and terminal-session failures instead of silently doing nothing after
  the review modal has already closed.

Manual QA performed against real claude and codex CLIs via the dev-server's
node-pty backend (see PR report for the full write-up): once an agent is
at its normal ready-to-chat input, writing the whole multi-line prompt as
one burst lands as unsent text with no premature submission. A first-run
"trust this directory?" onboarding screen (always hit by the scratch-
worktree path, since it is always a brand-new directory) is a separate,
unresolved risk this readiness wait does not cover, flagged in code
comments and the PR report for explicit sign-off before merge.

disabled
sortFindingsForReview lived in composables/useCommitReviewNav.ts, but
utils/reviewFixPrompt.ts (a pure utils module) needed the same order and
was importing it from there, inverting the established utils-do-not-
depend-on-composables direction (Task 0's unifiedDiff.ts/editableTarget.ts
precedent). Moves it to utils/reviewFindingsSort.ts; useCommitReviewNav.ts
re-exports it verbatim for back-compat with CommitReviewModal.vue's import.

disabled
Deliberate scope-narrowing for PR2, not a bug fix. Manual QA against real
claude/codex CLIs (see the previous commit's report) found that a brand-new
scratch worktree always hits a first-run "trust this directory?" onboarding
screen that misinterprets the piped fix prompt as menu navigation, which
drove a real brew upgrade --cask codex attempt in testing. The user was
shown this finding and decided to disable the scratch-worktree option for
this PR rather than ship it with that risk.

CommitReviewModal.vue's "Fix with agent" footer no longer offers a scratch
checkbox; its fix-with-agent emit payload drops the scratch flag. App.vue's
onCommitReviewFixWithAgent always targets the current repo (already
trusted, no onboarding screen) and no longer calls
createAiTaskScratchWorktree, which stays untouched for its other caller,
confirmNewAiTask ("New AI task"). Removed the now-unreachable
commitReview.fixInScratch i18n key from all 5 locales after confirming it
was not used anywhere else. Updated ROADMAP.md's Fix with agent bullet to
record the cut and the follow-up: revisit scratch-worktree support once
there is a real fix for the onboarding-trust-screen problem (pre-trusting
the directory before launching the agent, or detecting the onboarding
screen before writing).

disabled
…fore the trailer is written

Second verifier pass, HIGH: coverage:100% was still reachable in two real
scenarios even after the previous round's fix.

Scenario B: stage a.ts, review it (coverage 100), then edit a.ts further
and restage it. The staged file COUNT never changes (still one file, just
more content), so App.vue's staged-set watcher (keyed on repoStats.staged,
a count) never fires onStagedSetChanged, the coverage refresh never runs,
and the commit gets a stale coverage:100% for content that was never
reviewed. This is the single most common review-fix-review cycle.

Scenario C: run()'s own coverage computation compared the just-reviewed,
file/byte-capped subset against itself, a tautology that always yields
100% even when the file-count cap dropped most of a large staged tree.

Fix: a new shared pure helper, coverageFromDiffText, always computes
coverage against the FULL parsed staged diff, never the capped subset sent
to the AI. run() now uses it for both of its own coverage snapshots. A new
computeCurrentCoverage method wraps it with a fresh git diff fetch and is
awaited by App.vue's proceedToCommit right before buildReviewTrailer, so
the trailer's coverage number is correct at the moment of commit regardless
of staged-set-watcher granularity or truncation.

Also fixes a stale doc comment on createAiTaskScratchWorktree that still
claimed it was shared with Commit Review's scratch-worktree option (removed
in the previous commit) — it is now only used by "New AI task".

disabled
…nt finding text

Second verifier pass, low priority. sanitizeLine only stripped \r and
collapsed newlines before a finding's title/detail got written raw into a
PTY. That text is parsed from an AI response to an arbitrary diff, so a
crafted diff could in principle smuggle other control bytes (ANSI escape
sequences, NUL, BEL) into the raw PTY write. Broadens the strip to the full
C0 control range plus DEL after the existing \r/newline handling.

disabled
… 100 when disabled mid-cycle

Third verifier pass, item L1: computeCurrentCoverage and
refreshCoverageForCurrentDiff both defaulted next to 100 whenever the
feature was disabled or the diff fetch failed, discarding whatever
truthful coverage the ref already held. Toggling Commit Review off
between a real review and the commit could launder a genuinely partial
coverage back to a false 100% in the trailer, the same failure class
already fixed twice for other code paths in this PR.
@devlint

devlint commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #164, which combines all three PRs into a single review surface for the full v3.7.0 Commit Review feature.

@devlint devlint closed this Aug 19, 2026
devlint added a commit that referenced this pull request Aug 19, 2026
Combines the full v3.7.0 "Commit Review" feature, originally built and adversarially reviewed across three stacked PRs (#159, #160, #163), then further product- and code-reviewed as a whole and fixed here (see commit history for the detailed round-by-round verification trail).

- Review staged changes: AI pass over the staged diff, inline findings with severity badges, n/p/x navigation
- Fix with agent: pipe findings into a terminal AI agent session
- Iterations & coverage tracking, bound to HEAD
- Review / Vouch / Skip commit-time decision, recorded as a GitWand-Review trailer
- Per-repo .gitwandrc opt-in overriding the global setting in either direction
- Composable pre-commit hook merging the shipped secrets-scanner section with a new warn-only review reminder
- Fixed a core reactivity bug where routine background polling silently wiped findings, plus 13 other findings from a dedicated product/code review round

1067 core + 959 desktop tests, parity suite green, clean build, zero new Tauri commands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant