fix(workdir): use PR head.ref for worktree branch on PR-scoped dispatches #120
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!120
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/workdir-pr-head-ref"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #119.
Summary
Every PR-scoped dispatcher was passing
pr.numberasissue_numberthroughbuildAgentRequest, whichagent-runner.tsfed intoderiveIssueBranch(prefix, n)to compute the worktree's target branch. Ourimplementskill convention pushes to<prefix>/<issue_number>(keyed on the issue, not the PR), so PR #115 closing issue #26 had head branchboss/26while the dispatcher derivedboss/115. First dispatch created a stub local branch at HEAD, the skill swapped toboss/26inside the worktree, and the next dispatch'sacquireWorktreerejected the worktree as "on the wrong branch".Hit live yesterday on PR #115 — boss got stuck for an hour across multiple address-review retries until the operator manually cleaned worktrees and force-merged.
Fix — Option A from #119
Plumb
pull_request.head.reffrom the webhook payload through to a new optionalTaskRequest.branchfield.agent-runner.tspreferstask.branchwhen set, falling back toderiveIssueBranchfor issue-scoped dispatches and the/taskHTTP endpoint where no PR context exists.Changes
worker.ts—TaskRequest.branch?: stringadded with a full docstring tying to #119.agent-runner.ts— new exportedresolveDispatchBranch(task, prefix)helper;runAgentTaskuses it instead of the inlinedtask.branch || deriveIssueBranch(...).webhook-handlers.ts— every PR-scoped handler now acceptshead?: { ref?: string }on itsprparam and passes{ branch: pr.head?.ref }throughbuildAgentRequest's extras.handleReviewRequestedprefers the PR detail's head.ref it already fetches, falls back to the webhook payload.webhook-ci.ts—PrForCigrowshead?: { ref?: string };buildAgentRequesttakes an optionalbranch;dispatchFixCi+dispatchMergepasspr.head?.ref.handlePullRequestOpenedforwardshead.refin thePrForCistub it constructs fordecidePostCiAction.agent-runner.test.ts— 4 new tests onresolveDispatchBranch: override path, fallback path, empty-string fallback, prefix-decoupling fromtask.branch_prefix.No behaviour change for issue-scoped dispatches
Issue-scoped dispatchers (
handleIssueAssigned, the/taskHTTP endpoint) don't settask.branch, soresolveDispatchBranchfalls through toderiveIssueBranch— identical to the old inlined code.Test plan
bunx tsc --noEmit— cleanbunx biome check src/— cleanbun test src/agent-runner.test.ts— 50 pass (4 new)bun test— 452 pass, 1 fail (pre-existing env-dependentdocker volume rmtest inmain-agents.test.ts, unrelated to this PR)Out of scope
git worktree removewhen a stale one bites; a one-shot sweep recipe (just worktrees-prune-stale) is a nice-to-have follow-up.worktreePath(agent, repo, branch)remains branch-keyed. Only the branch input changes; the dir name follows.🤖 Generated with Claude Code
Round 1 — APPROVED
CI: Green (run #1683, 2m57s)
Summary
Fix is correct and complete. All PR-scoped dispatch paths now pass
head.reftoresolveDispatchBranch, which prevents the wrong-branch worktree error that hit live on PR #115.Correctness walkthrough
resolveDispatchBranch(agent-runner.ts)task.branch || deriveIssueBranch(branchPrefix, task.issue_number)— correctly prefers an explicit branch override. The||operator handles empty-string the same as undefined (tested: "falls back when task.branch is empty string"). ✓All 6 PR-scoped dispatch paths wired:
handleReviewRequested—prDetail?.head?.ref ?? pr.head?.refdispatchRebaseIfNotMergeable—pr.head?.refhandleChangesRequested—pr.head?.refhandleApproved→dispatchMerge—pr.head?.refviaPrForCidispatchFixCi—pr.head?.refdecidePostCiAction→dispatchMerge—head.reffromOpenPRForHead(always present)handleApprovedonly updates the type signature — branch flows through todispatchRebaseIfNotMergeableanddispatchMerge, both of which now accept and usePrForCi.head.ref. Full function verified: it callsdispatchMerge(repo, pr)whereprcarrieshead?.ref. ✓Issue-scoped dispatches unaffected:
task.branchisundefined→ falls through toderiveIssueBranch. ✓Type safety: All four handler signatures updated consistently with
head?: { ref?: string }. The Forgejo webhook payload type atwebhook.ts:93already declareshead?: { ref: string; sha?: string }so the value is always available from the payload. ✓Tests: 4 new
resolveDispatchBranchtests cover override path, fallback path, empty-string fallback, prefix-decoupling. ✓AC status (#119)
acquireWorktreemismatch — Not added. Option A prevents the scenario from occurring, and the existing error is already actionable. Acceptable follow-up.issue_number=115 → branch=boss/26 via PR head.refin journalctl. Current code logsacquiring worktree boss/26(effective branch visible but source not explicit). Acceptable follow-up.The two open ACs are observability improvements, not correctness gaps. Core bug is fixed and all dispatch paths are correctly wired.
Round 1 — APPROVED
CI: ✅ Green (run #1683, 2m57s)
Summary
Fix is correct and complete. All PR-scoped dispatch paths now pass
head.reftoresolveDispatchBranch, which prevents the wrong-branch worktree error that hit live on PR #115.Correctness walkthrough
resolveDispatchBranch(agent-runner.ts)task.branch || deriveIssueBranch(branchPrefix, task.issue_number)— correctly prefers an explicit branch override. The||operator handles empty-string the same as undefined (tested: "falls back when task.branch is empty string"). ✓All 6 PR-scoped dispatch paths wired:
handleReviewRequestedprDetail?.head?.ref ?? pr.head?.refdispatchRebaseIfNotMergeablepr.head?.refhandleChangesRequestedpr.head?.refhandleApproved→dispatchMergepr.head?.refviaPrForCidispatchFixCipr.head?.refdecidePostCiAction→dispatchMergehead.reffromOpenPRForHead(always present)handleApprovedonly updates the type signature — branch flows through todispatchRebaseIfNotMergeableanddispatchMerge, both of which now accept and usePrForCi.head.ref. Full function verified: it callsdispatchMerge(repo, pr)whereprcarrieshead?.ref. ✓Issue-scoped dispatches unaffected:
task.branchisundefined→ falls through toderiveIssueBranch. ✓Type safety: All four handler signatures updated consistently with
head?: { ref?: string }. The Forgejo webhook payload type atwebhook.ts:93already declareshead?: { ref: string; sha?: string }so the value is always available. ✓Tests: 4 new
resolveDispatchBranchtests cover override path, fallback path, empty-string fallback, prefix-decoupling. ✓AC status (#119)
acquireWorktreemismatch — Not added. Option A prevents the scenario from occurring, and the existing error message is already actionable (worktree at X is on branch Y, expected Z — release first). Acceptable follow-up.issue_number=115 → branch=boss/26 via PR head.refin journalctl. Current code logsacquiring worktree boss/26(effective branch visible but derivation source not explicit). Acceptable follow-up.The two open ACs are observability improvements, not correctness gaps. Core bug is fixed and all dispatch paths are correctly wired.