Skip to content

Commit dce1723

Browse files
GiggleLiuclaude
andcommitted
fix: auto-request Copilot reviews and handle existing PRs in pipeline
- Add `request_copilot_reviews` to make_helpers.sh; `run-review-forever` now auto-requests Copilot review on waiting PRs before each poll cycle - Add `worktree-for-issue` command to pipeline_worktree.py that checks for an existing open PR and checks out its branch instead of creating a fresh worktree from main - Fix `fetch_existing_prs` search string ("Fixes #N" → "Fix #N") to match actual PR title convention - Update project-pipeline skill to use `worktree-for-issue` and request Copilot review after moving to Review pool - Update CLAUDE.md and review-pipeline skill docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0474f09 commit dce1723

7 files changed

Lines changed: 108 additions & 11 deletions

File tree

.claude/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ make run-pipeline N=97 # Process a specific issue from the project board
6262
make run-pipeline-forever # Poll Ready column, run-pipeline when new issues appear
6363
make run-review # Pick next PR from Review pool column, fix Copilot comments, fix CI, run agentic tests
6464
make run-review N=570 # Process a specific PR from the Review pool column
65-
make run-review-forever # Poll Review pool for Copilot-reviewed PRs, run-review when new ones appear
66-
make copilot-review # Request Copilot code review on current PR
65+
make run-review-forever # Poll Review pool, auto-request Copilot reviews, dispatch run-review when reviewed
66+
make copilot-review # Request Copilot code review on current PR (requires: gh extension install ChrisCarini/gh-copilot-review)
6767
make release V=x.y.z # Tag and push a new release (CI publishes to crates.io)
6868
# Set RUNNER=claude to use Claude instead of Codex (default: codex)
6969
# Default Codex model: CODEX_MODEL=gpt-5.4

.claude/skills/project-pipeline/SKILL.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,19 @@ TITLE=$(printf '%s\n' "$CLAIM" | python3 -c "import sys,json; print(json.load(sy
136136

137137
### 1. Create Worktree
138138

139-
Create an isolated git worktree for this issue so the main working directory stays clean:
139+
Create an isolated git worktree for this issue. The script automatically checks for an existing open PR — if one exists, it checks out that PR branch (treating it as an incomplete implementation); otherwise it creates a fresh worktree from `origin/main`:
140140

141141
```bash
142-
WORKTREE=$(python3 scripts/pipeline_worktree.py create-issue --issue "$ISSUE" --slug <slug> --base origin/main --format json)
143-
BRANCH=$(printf '%s\n' "$WORKTREE" | python3 -c "import sys,json; print(json.load(sys.stdin)['branch'])")
142+
WORKTREE=$(python3 scripts/pipeline_worktree.py worktree-for-issue \
143+
--repo "$REPO" --issue "$ISSUE" --slug <slug> --format json)
144+
ACTION=$(printf '%s\n' "$WORKTREE" | python3 -c "import sys,json; print(json.load(sys.stdin)['action'])")
144145
WORKTREE_DIR=$(printf '%s\n' "$WORKTREE" | python3 -c "import sys,json; print(json.load(sys.stdin)['worktree_dir'])")
145146
cd "$WORKTREE_DIR"
146147
```
147148

149+
- `action == "resume-pr"`: existing PR checked out — `issue-to-pr` will skip plan creation and jump to execution
150+
- `action == "create-worktree"`: fresh branch from `origin/main`
151+
148152
All subsequent steps run inside the worktree. This ensures the user's main checkout is never modified.
149153

150154
### 2. Claim Result
@@ -159,18 +163,21 @@ Invoke the `issue-to-pr` skill with `--execute` (working directory is the worktr
159163
/issue-to-pr "$ISSUE" --execute
160164
```
161165

162-
This handles the full pipeline: fetch issue, verify Good label, research, write plan, create PR, implement, review, fix CI.
166+
This handles the full pipeline: fetch issue, verify Good label, research, write plan, create PR, implement, review, fix CI. If an existing PR was detected in Step 1, `issue-to-pr` will resume it (skip plan creation, jump to execution).
163167

164168
**If `issue-to-pr` fails after creating a PR:** record the failure, but still move the issue to "Final review" so it's visible for human triage. Report the failure to the user.
165169

166170
### 4. Move to "Review pool"
167171

168-
After `issue-to-pr` succeeds, move the issue to the `Review pool` column for the second-stage review pipeline:
172+
After `issue-to-pr` succeeds, move the issue to the `Review pool` column and request a Copilot review so the review pipeline can pick it up:
169173

170174
```bash
171175
python3 scripts/pipeline_board.py move <ITEM_ID> review-pool
176+
gh copilot-review <PR_NUMBER>
172177
```
173178

179+
The Copilot review request is required — without it, `run-review-forever` will not detect the PR as eligible.
180+
174181
**If `issue-to-pr` failed after creating a PR:** move the issue to `Final review` instead so a human can take over:
175182

176183
```bash
@@ -237,3 +244,4 @@ Completed: 2/4 | Review pool: 3 | Returned to Ready: 1
237244
| Worktree left behind on failure | Always clean up with `git worktree remove` in Step 5 |
238245
| Working in main checkout | All work happens in `.worktrees/` — never modify the main checkout |
239246
| Missing items from project board | `gh project item-list` defaults to 30 items — always use `--limit 500` |
247+
| Creating a fresh branch when PR exists | Check `issue-context` action field first — use `checkout-pr` for existing PRs instead of `create-issue` |

.claude/skills/review-pipeline/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ Completed: 2/2 | All moved to Final review
288288
| PR not in Review pool column | Verify status before processing; STOP if not Review pool |
289289
| Processing a closed PR from a stale issue card | Require PR state `OPEN`; skip stale closed PRs |
290290
| Guessing on an issue card with multiple linked repo PRs | Stop, show options to the user, and recommend the most likely correct OPEN PR |
291-
| Picking a PR before Copilot has reviewed | Check `pulls/$PR/reviews` for copilot-pull-request-reviewer[bot]; skip if absent |
291+
| Picking a PR before Copilot has reviewed | Check `pulls/$PR/reviews` for copilot-pull-request-reviewer[bot]; if absent, request with `gh copilot-review <PR>` and wait |
292292
| Missing project scopes | Run `gh auth refresh -s read:project,project` |
293293
| Skipping review-implementation | Always run structural completeness check in Step 2b — it catches gaps Copilot misses (paper entries, CLI registration, trait_consistency) |
294294
| Skipping agentic tests | Always run test-feature even if CI is green |

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,8 +577,8 @@ run-review:
577577
PROMPT=$$(skill_prompt_with_context review-pipeline "$$slash_cmd" "$$codex_desc" "Review pipeline context" "$$selection"); \
578578
run_agent "review-output.log" "$$PROMPT"
579579

580-
# Poll Review pool column for Copilot-reviewed PRs and run-review when new ones appear
581-
# Checks every 10 minutes; triggers make run-review when the eligible PR set gains new members
580+
# Poll Review pool column for PRs and run-review when Copilot-reviewed ones appear
581+
# Auto-requests Copilot review on PRs that don't have one yet before each poll cycle
582582
run-review-forever:
583583
@. scripts/make_helpers.sh; \
584584
REPO=$$(gh repo view --json nameWithOwner --jq .nameWithOwner); \

scripts/make_helpers.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,24 @@ cleanup_pipeline_worktree() {
204204
python3 scripts/pipeline_worktree.py cleanup --worktree "$worktree" --format json
205205
}
206206

207+
# Request Copilot review on all Review pool PRs that don't have one yet.
208+
# request_copilot_reviews <repo>
209+
request_copilot_reviews() {
210+
repo=$1
211+
prs=$(python3 scripts/pipeline_board.py list review --repo "$repo" --format json \
212+
| python3 -c "
213+
import sys, json
214+
data = json.load(sys.stdin)
215+
for item in data.get('items', []):
216+
if item.get('eligibility') == 'waiting-for-copilot' and item.get('pr_number'):
217+
print(item['pr_number'])
218+
")
219+
for pr in $prs; do
220+
echo "$(date '+%Y-%m-%d %H:%M:%S') Requesting Copilot review on PR #${pr}..."
221+
gh copilot-review "$pr" 2>&1 || true
222+
done
223+
}
224+
207225
# Poll a board column and dispatch a make target when new items appear.
208226
# watch_and_dispatch <mode> <make-target> <label> [repo]
209227
# Example:
@@ -221,6 +239,11 @@ watch_and_dispatch() {
221239

222240
echo "Watching for new ${label} (polling every $((interval / 60))m)..."
223241
while true; do
242+
# For review mode, request Copilot reviews on PRs that don't have one yet
243+
if [ "$mode" = "review" ] && [ -n "$repo" ]; then
244+
request_copilot_reviews "$repo"
245+
fi
246+
224247
next_item=$(poll_project_items "$mode" "$state_file" "$repo")
225248
status=$?
226249
if [ "$status" -eq 0 ]; then

scripts/pipeline_checks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ def fetch_existing_prs(repo: str, issue_number: int) -> list[dict]:
596596
"--state",
597597
"open",
598598
"--search",
599-
f"Fixes #{issue_number}",
599+
f"Fix #{issue_number}",
600600
"--json",
601601
"number,headRefName,url",
602602
)

scripts/pipeline_worktree.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,51 @@ def prepare_review(
259259
}
260260

261261

262+
def worktree_for_issue(
263+
*,
264+
repo: str,
265+
issue_number: int,
266+
slug: str,
267+
base_ref: str = "origin/main",
268+
repo_root: str | Path | None = None,
269+
) -> dict:
270+
"""Create or checkout a worktree for an issue.
271+
272+
If the issue already has an open PR, checkout the PR branch.
273+
Otherwise, create a fresh worktree from base_ref.
274+
"""
275+
import pipeline_checks
276+
277+
existing_prs = pipeline_checks.fetch_existing_prs(repo, issue_number)
278+
if existing_prs:
279+
pr = existing_prs[0]
280+
pr_number = int(pr["number"])
281+
result = checkout_pr_worktree(
282+
repo=repo,
283+
pr_number=pr_number,
284+
repo_root=repo_root,
285+
)
286+
return {
287+
**result,
288+
"action": "resume-pr",
289+
"issue_number": issue_number,
290+
"pr_number": pr_number,
291+
}
292+
293+
result = create_issue_worktree(
294+
issue_number=issue_number,
295+
slug=slug,
296+
base_ref=base_ref,
297+
repo_root=repo_root,
298+
)
299+
return {
300+
**result,
301+
"action": "create-worktree",
302+
"issue_number": issue_number,
303+
"pr_number": None,
304+
}
305+
306+
262307
def cleanup_worktree(*, worktree: str | Path) -> dict:
263308
worktree = Path(worktree).resolve()
264309
repo_root = repo_root_from(worktree)
@@ -302,6 +347,14 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
302347
checkout_pr.add_argument("--repo-root")
303348
checkout_pr.add_argument("--format", choices=["json", "text"], default="json")
304349

350+
for_issue = subparsers.add_parser("worktree-for-issue")
351+
for_issue.add_argument("--repo", required=True)
352+
for_issue.add_argument("--issue", required=True, type=int)
353+
for_issue.add_argument("--slug", required=True)
354+
for_issue.add_argument("--base", default="origin/main")
355+
for_issue.add_argument("--repo-root")
356+
for_issue.add_argument("--format", choices=["json", "text"], default="json")
357+
305358
prepare_review = subparsers.add_parser("prepare-review")
306359
prepare_review.add_argument("--repo", required=True)
307360
prepare_review.add_argument("--pr", required=True, type=int)
@@ -346,6 +399,19 @@ def main(argv: list[str] | None = None) -> int:
346399
)
347400
return 0
348401

402+
if args.command == "worktree-for-issue":
403+
emit_result(
404+
worktree_for_issue(
405+
repo=args.repo,
406+
issue_number=args.issue,
407+
slug=args.slug,
408+
base_ref=args.base,
409+
repo_root=args.repo_root,
410+
),
411+
args.format,
412+
)
413+
return 0
414+
349415
if args.command == "checkout-pr":
350416
emit_result(
351417
checkout_pr_worktree(

0 commit comments

Comments
 (0)