feat: keep PR review worktrees on the current GitHub head - #20
Conversation
Fast-forward idle review checkouts when GitHub's head moves, skip local edits and live agents, and prompt before reset --hard on rewritten history.
Functional tests and a second daemon need their own config directory. Existing worktrees were ignoring --json/--no-terminal and always opening a tab.
On Darwin, ps -o comm= is the executable path, not the basename. Live-agent detection then missed /opt/homebrew/bin/claude and any cwd-based skip.
62c188b to
221ac05
Compare
GitHub Actions has no user.name; commits in a fresh clone were failing.
|
thanks, good idea as i ended up having to ask claude or codex to pull the latest updates after a zen review resume. I've merged another PR and therefore there are some conflicts, can you please resolve them. thanks |
Keep worktree refresh (PollMemory, HeadRefOid, ZEN_HOME) together with Slack polling, zen board Kind tags, and CreateFromPR.
mgreau
left a comment
There was a problem hiding this comment.
The shared refresh path, dirty-worktree checks, and cross-repository PR keys are useful improvements. The full Go test suite, go vet, and targeted race tests passed locally. I found three issues to fix before merging, detailed inline: noninteractive hard resets, false update reports after a backward force-push, and configured PRs being hidden by the global search limit.
For release sequencing, I plan to land this after #23/#24, then rebase #13 onto it. The #13 integration must select the configured repository's remote in both CreateFromPR and the new FetchPRHead/FetchRefspec refresh path. Updating only creation would leave refresh fetching from origin in fork clones.
When #25 lands, the new worktree lookup in watch.pollOnce must also use worktree.Resolve; otherwise nested worktrees created for authors outside the allowlist are treated as missing and never refreshed. Please preserve that requirement during the rebase.
Please add regression coverage for the cases below. Release notes should also tell users to restart zen watch so the new refresh behavior is active.
| // confirmResetWorktree asks before git reset --hard onto a rewritten GitHub | ||
| // head. --json never resets (no TTY). Default is no. | ||
| func confirmResetWorktree(req review.ResetRequest) bool { | ||
| if jsonFlag { |
There was a problem hiding this comment.
[P1] Require an actual terminal before offering a hard reset. This only checks --json; fmt.Scanln below also accepts piped input. In a disposable repository with diverged PR history, I ran the built CLI with piped yes and --no-terminal, and it executed reset --hard without a TTY. Please reject noninteractive stdin here and treat input errors/EOF as a declined reset. Add a regression case with piped input, alongside the interactive confirmation case.
| return maybeReset(gitCtx, worktreePath, prNumber, log, confirmReset) | ||
| } | ||
| log(fmt.Sprintf("fast-forwarded PR #%d worktree", prNumber)) | ||
| return SyncUpdated, nil |
There was a problem hiding this comment.
[P2] Verify that HEAD actually reaches the fetched target before returning SyncUpdated. If the PR is force-pushed backward from B to ancestor A, git merge --ff-only A exits successfully with "Already up to date" and leaves B checked out. I reproduced that behavior. This path then reports a successful update, rewrites context, and can send another update notification on every poll, while the CLI never offers the reset needed to reach A. Detect this ancestor/ahead case and route it through the same reset-or-skip policy as diverged history; report SyncUpdated only when HEAD moved to the target. Please cover both a backward force-push and local commits ahead of the fetched ref.
| func pollOnce(ctx context.Context, seenPRs map[string]bool, queue workqueue.Interface, rec *reconciler.SetupReconciler) { | ||
| reviews, err := ghpkg.GetReviewRequests(ctx, "chainguard-dev/mono", cfg.IgnoreDrafts) | ||
| func pollOnce(ctx context.Context, st *reconciler.PollMemory, queue workqueue.Interface, rec *reconciler.SetupReconciler) { | ||
| reviews, err := ghpkg.GetReviewRequests(ctx, "", cfg.IgnoreDrafts) |
There was a problem hiding this comment.
[P2] Scope or paginate the search before filtering repositories locally. Passing an empty repo filter makes both GetReviewRequests searches global, but each query still uses first: 50 without pagination. Requests in unconfigured repositories can occupy the returned slots, so a configured repository's PR can disappear from this poll even when it would have appeared in the previous repository-scoped query. That PR then receives neither setup nor refresh. Query configured repositories and handle pagination, or paginate a suitably scoped combined search. Please cover more than 50 results, including requests from unconfigured repositories.
Summary
Review worktrees used to be create-once. After this, an idle checkout tracks GitHub's current head.
Each poll compares
headRefOidto worktreeHEAD. Linear updates aregit fetchintorefs/remotes/origin/pr-N(not the checked-outpr-Nbranch) thengit merge --ff-only. Context is rewritten only when HEAD actually moved.authors:.zen reviewcan create those trees and watch still keeps them current.chainguard-dev/mono).Safety
Unattended paths (watch, MCP,
zen review --json) nevergit reset --hard.claude/codexcwd in the tree)/review-prwith no UUID yetCLAUDE.local.md/.zen/zen review/zen review resumeprompt[y/N](default no). Reflog keeps the old tip.On macOS,
ps -o comm=is the executable path. Live-agent detection usesfilepath.Baseso/opt/homebrew/bin/claudeis not missed.zen review --json/--no-terminalon an existing worktree refreshes and does not open a tab.ZEN_HOMEoverrides~/.zenso a second daemon or a functional harness does not shareconfig.yaml/last_check.json.Notifications
authors:.seen_prsis absorbed intonotified_newon the first poll, then dropped).Upgrade
Restart
zen watchso the new binary is what polls.config.yamldoes not change. Existing worktrees stay in place; the next poll fast-forwards idle ones.Closes #18.
Functional test
Ran end-to-end against imkarrer/zen-tests (
./scripts/keep-worktrees.sh) with an isolatedZEN_HOMEso day-to-day~/.zenis untouched. 21/21 passed (create, A→B FF + context rewrite, dirty skip, live-agent skip,--jsonno reset, TTY confirm reset, inbox notify without auto-create,seen_prsupgrade, watch create + FF, draft left alone, undraft catch-up without a second “new”).Merged cleanup is still a unit test (
cleanup_after_dayscannot be 0).Test plan
zen-tests)ignore_drafts: true) → push C → undraft → one refresh to C, not a second “new review request”zen reviewon an existing directory fetches before opening a tab (--no-terminal/--jsondo not open a tab)--jsonskip reset; TTY asksauthors:and has no worktree yetseen_prsdoes not re-announce current inbox PRsNote for reviewers
Conflicts with #13 (
git fetch originvs remote matchingfull_name). This PR should land first or rebase onto #13 and useRemoteForRepoinFetchPRHead.