fix(webhook): pivot implement → address-review on outstanding REQUEST_CHANGES #271
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!271
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/270"
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?
Summary
Closes #270.
When
handleIssueAssignedre-fires for an issue whose linked PR isalready awaiting review changes (operator assignee-bounce, dependency-
propagator unblock, label re-add),
dispatchIssueForAgentnow probesthe linked PR. If the latest submitted verdict is
REQUEST_CHANGESagainst the PR's current head SHA, the dispatch is pivoted from
implementtoaddress-reviewand the worktree is aimed at the PR'shead ref (e.g.
dev/262), not the derived<prefix>/<issue>.Before this, the dev agent would read the issue body, see "PR exists +
CI green", run QA, declare "work complete", and exit — head SHA
never changed, reviewer never re-fired (no new commit to look at), the
issue sat in IN_REVIEW forever (2026-04-21 incident on #262 / PR #266,
7+ implement dispatches after REQUEST_CHANGES, zero new commits).
Pieces
webhook-handlers.ts::detectOutstandingChangeRequest) —locates the linked PR via the branch convention
(
<branch_prefix>/<issue_number>), pulls its reviews, walks themreverse-chronologically to find the latest
APPROVED/REQUEST_CHANGESverdict. Returns
{ prNumber, headRef, headSha }only when the latestverdict is
REQUEST_CHANGESand itscommit_idequals the PR'scurrent
head.sha. Every other case (no PR, no verdict, APPROVED,stale REQUEST_CHANGES on an older SHA the author has since pushed
past) returns
nulland the defaultimplementdispatch runs.dispatchIssueForAgent— scoped tobaseSkill === "implement"(thereviewlabel route forarea:design-reviewstays as-is). Errors in the probe are swallowed with a warning; the
skill-side guard picks up the slack.
skills/implement.md— a preconditionsection instructs the agent to
mcp__forgejo__list_repo_pull_requestsmcp__forgejo__list_pull_reviewsand STOP with a🛑 implement skill misroutedcomment if the detector missed (e.g.non-standard branch).
propagateDependencyClosurestill calls
handleIssueAssignedon the assigned-dependent branch;the pivot applies automatically because it lives in
dispatchIssueForAgent.Tests
latestVerdict— reducer unit tests (empty, ignore REQUEST_REVIEW /COMMENT, chronological latest wins, skip missing
commit_id).detectOutstandingChangeRequest— full decision table: no PR, noreview, APPROVED, REQUEST_CHANGES @ head, REQUEST_CHANGES @ stale,
REQUEST_CHANGES followed by APPROVED, PR with non-matching branch ref.
dispatchIssueForAgent— end-to-end: captures the dispatched taskbody and asserts
Address review feedback on PR #NvsImplement issue #Nplusrequest.branch. Also confirmsbaseSkill=reviewis never pivoted.propagateDependencyClosurere-fire lands onaddress-reviewwhen the dependent has an outstanding REQUEST_CHANGES PR.Test plan
bun x turbo run test— 944 passed, 0 failedbun x biome check .cleanbun x biome format .cleanbun x tsc --noEmiton@claude-hooks/servercleanreview comment and pushes a commit addressing the 3 findings
(reviewer loop gate unchanged, so CI → reviewer → merge flow
should carry through without further operator input).
🤖 Generated with Claude Code
Review — fix(webhook): pivot implement → address-review on outstanding REQUEST_CHANGES
CI ✅ green (#1950, 4m16s). Round 1 full review.
Acceptance criteria check (issue #270)
dispatchIssueForAgentprobes linked PR; pivots toaddress-reviewwhen PR open + latest verdictREQUEST_CHANGES@ current head SHAtask.branchset to PR's head ref on pivot (not derived issue branch)dependent_unblockedpath benefits automaticallyhandleIssueAssigned→dispatchIssueForAgent)skills/implement.mddefensive guard addedlist_repo_pull_requests, callslist_pull_reviews, stops + comments only when latest verdict isREQUEST_CHANGESandcommit_id === head.shalatestVerdictdetectOutstandingChangeRequestsuitedispatchIssueForAgent: asserts task body + branchpropagateDependencyClosuredescribe blockCode correctness
latestVerdict— reverse iteration is correct; Forgejo chronological order means last element = most recent. The "latest wins" semantics (APPROVED after REQUEST_CHANGES clears the block) match the documented design choice. Multi-reviewer edge case (different reviewers, last one APPROVED) is correctly handled the same way — if Reviewer B approved after Reviewer A requested changes,implementnotaddress-reviewis the right choice.findLinkedPrForIssue— stricthead.ref === deriveIssueBranch(prefix, issue)matching is the right call: conservative, avoids false pivots on coincidentally-numbered unrelated PRs. Non-standard branches fall through to the skill-side guard as documented.Error swallow in
dispatchIssueForAgent(lines 1027–1034) — deliberate and well-documented. The skill-side guard inimplement.mdis the correct second line of defence. The only missing test is for the error-swallow path itself (confirm implement fires after the probe throws), but it's not required by the ACs and the fallback is trivially correct.prNumberForTemplate— correctly set tooutstanding.prNumberon pivot, soaddress-review.md's{{pr_number}}resolves to the actual PR number, not the issue number. ✅stateless_session—STATELESS_SKILLS.has("address-review")isfalse, so the pivot path is session-resumable. In practice the session key is(type, repo, issue.number)— same key as the priorimplementdispatches — soskillForEventmay find an existing session and loadaddress-review-delta.mdrather than the full template. This differs from the normalhandleReviewRequestedpath (which keys offpr.numberand therefore always starts fresh). In practiceaddress-review.mdinstructs the agent tolist_pull_reviewsfresh regardless of session history, so correctness is unaffected; the worst case is a marginally odd conversation preamble. Not a blocker — the session-key split between issue-scoped and PR-scoped dispatch is a pre-existing design boundary.Skill guard
skills/implement.mdguard text is tight and correct:head branch ends in /{{issue_number}}as the secondary match (catches non-standard bodies without Closes #N)REQUEST_REVIEW/COMMENT/pending entries — consistent withlatestVerdict🛑 implement skill misrouted — …) so the operator sees the signal even if the webhook never re-dispatchesLGTM. The fix directly closes the 2026-04-21 incident root cause, the tests cover the full decision table, and the skill-side guard provides a second-line safety net for the cases the detector can miss (non-standard branch, transient network error).