Skip to content

Commit 3dcc4e3

Browse files
penny-team[bot]jaredlockhartclaude
authored
Skip bug issues that already have an open PR (jaredlockhart#784) (jaredlockhart#784)
Worker was starting duplicate work on bug issues that already had an open PR. The bug fix workflow (Step 2) had no PR-awareness, unlike the in-progress workflow (Step 3). Fix in Python-space: enrich bug issues with open-PR detection and skip them in pick_actionable_issue. Co-authored-by: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e1a08ad commit 3dcc4e3

5 files changed

Lines changed: 142 additions & 6 deletions

File tree

penny-team/penny_team/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def run(self) -> AgentRun:
349349
self.required_labels, trusted_users=self.trusted_users, api=self.github_api
350350
)
351351

352-
# Enrich in-review issues with CI and merge conflict status (no-op if none match)
352+
# Enrich issues: CI/merge status for in-review, open-PR detection for bugs
353353
from penny_team.utils.pr_checks import enrich_issues_with_pr_status
354354

355355
processed = self._load_processed()

penny-team/penny_team/utils/issue_filter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class FilteredIssue:
4141
ci_failure_details: str | None = None
4242
merge_conflict: bool = False
4343
merge_conflict_branch: str | None = None
44+
has_open_pr: bool = False
4445
has_review_feedback: bool = False
4546
review_comments: str | None = None
4647

@@ -156,6 +157,10 @@ def pick_actionable_issue(
156157
sorted_issues = sorted(issues, key=lambda i: TeamConstants.Label.BUG not in i.labels)
157158

158159
for issue in sorted_issues:
160+
# Bug already has an open PR — worker would duplicate work
161+
if issue.has_open_pr:
162+
continue
163+
159164
# Check if external signals require attention regardless of comments
160165
if issue.ci_status == TeamConstants.CI_STATUS_FAILING:
161166
return issue

penny-team/penny_team/utils/pr_checks.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@ def enrich_issues_with_pr_status(
4040
bot_logins: set[str] | None = None,
4141
processed_at: dict[str, str] | None = None,
4242
) -> None:
43-
"""Enrich in-review issues with CI check and merge conflict status from their PRs.
43+
"""Enrich issues with PR status: CI checks, merge conflicts, and open-PR detection.
4444
45-
Mutates FilteredIssue objects in place, setting ci_status,
46-
ci_failure_details, merge_conflict, and merge_conflict_branch.
45+
For in-review issues: sets ci_status, ci_failure_details, merge_conflict,
46+
merge_conflict_branch, has_review_feedback, and review_comments.
47+
For bug issues: sets has_open_pr when a matching PR already exists,
48+
so the worker skips duplicate work.
4749
When processed_at is provided, only review feedback newer than
4850
the agent's last processing time is included (prevents re-addressing
4951
already-handled comments).
5052
Fail-open: if the API fails, issues are left unchanged.
5153
"""
5254
in_review = [i for i in issues if TeamConstants.Label.IN_REVIEW in i.labels]
53-
if not in_review:
55+
bugs = [i for i in issues if TeamConstants.Label.BUG in i.labels]
56+
if not in_review and not bugs:
5457
return
5558

5659
try:
@@ -59,6 +62,12 @@ def enrich_issues_with_pr_status(
5962
logger.warning("Failed to fetch PR statuses, skipping CI/merge detection")
6063
return
6164

65+
# Flag bug issues that already have an open PR (prevents duplicate work)
66+
_flag_bugs_with_open_prs(prs, bugs)
67+
68+
if not in_review:
69+
return
70+
6271
pr_by_issue = _match_prs_to_issues(prs, in_review)
6372

6473
for issue in in_review:
@@ -120,6 +129,24 @@ def enrich_issues_with_pr_status(
120129
issue.ci_failure_details = details
121130

122131

132+
def _flag_bugs_with_open_prs(
133+
prs: list[PullRequest],
134+
bugs: list[FilteredIssue],
135+
) -> None:
136+
"""Flag bug issues that already have an open PR matching their issue number."""
137+
if not bugs:
138+
return
139+
pr_by_issue = _match_prs_to_issues(prs, bugs)
140+
for issue in bugs:
141+
if issue.number in pr_by_issue:
142+
issue.has_open_pr = True
143+
pr = pr_by_issue[issue.number]
144+
logger.info(
145+
f"Bug #{issue.number} already has open PR "
146+
f"#{pr.number} ({pr.head_ref_name}), skipping"
147+
)
148+
149+
123150
def _fetch_open_prs(
124151
api: GitHubAPI | None = None,
125152
) -> list[PullRequest]:

penny-team/penny_team/worker/CLAUDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,16 @@ If no merge conflicts and no unaddressed review comments, check the pre-fetched
194194
195195
Look at the pre-fetched issues for any with the `bug` label. Bug issues are prioritized over `in-progress` features.
196196
197-
If a `bug` issue exists, follow the **Bug Fix Workflow** below instead of the normal feature workflow.
197+
If a `bug` issue exists:
198+
- Check if a PR already exists for it:
199+
```bash
200+
gh pr list --state open --json number,title,headRefName --limit 10
201+
```
202+
- **PR exists for this bug** → Move to `in-review` and exit:
203+
```bash
204+
gh issue edit <N> --remove-label bug --add-label in-review
205+
```
206+
- **No PR exists** → Follow the **Bug Fix Workflow** below.
198207
199208
#### Bug Fix Workflow
200209

penny-team/tests/test_worker.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,101 @@ def test_bug_issue_triggers_worker(
867867
assert "Search fails when query is empty" in prompt
868868
assert "Issue #50" in prompt
869869

870+
def test_bug_with_open_pr_skipped(
871+
self, tmp_path, mock_github_api, capture_popen, monkeypatch
872+
):
873+
"""Bug issue that already has an open PR → skipped (no duplicate work).
874+
875+
Flow: issue #50 is labeled 'bug', PR #10 exists on branch issue-50-fix
876+
→ enrich_issues_with_pr_status flags has_open_pr=True
877+
→ pick_actionable_issue skips it → agent does not run Claude.
878+
"""
879+
agent = make_agent(
880+
tmp_path, name="worker", required_labels=["in-progress", "in-review", "bug"],
881+
github_api=mock_github_api,
882+
)
883+
monkeypatch.setattr(type(agent), "_bot_logins", property(lambda self: BOT_LOGINS))
884+
885+
mock_github_api.set_issues("in-progress", [])
886+
mock_github_api.set_issues("in-review", [])
887+
mock_github_api.set_issues("bug", make_issue_list_items((50, "2024-01-01T00:00:00Z")))
888+
mock_github_api.set_issues_detailed("in-progress", [])
889+
mock_github_api.set_issues_detailed("in-review", [])
890+
mock_github_api.set_issues_detailed("bug", [
891+
make_issue_detail(
892+
number=50,
893+
title="Search fails when query is empty",
894+
body="When a user sends an empty search query, the app crashes.",
895+
labels=["bug"],
896+
comments=[],
897+
),
898+
])
899+
mock_github_api.set_prs([
900+
make_pull_request(10, "issue-50-fix-empty-search"),
901+
])
902+
903+
calls = capture_popen(stdout_lines=[result_event()], returncode=0)
904+
905+
result = agent.run()
906+
907+
assert result.success is True
908+
assert result.output == "No actionable issues"
909+
assert len(calls) == 0
910+
911+
def test_bug_with_open_pr_falls_through_to_feature(
912+
self, tmp_path, mock_github_api, capture_popen, monkeypatch
913+
):
914+
"""Bug has open PR, but in-progress feature exists → works on feature.
915+
916+
Flow: bug #50 has open PR → skipped, but feature #43 is actionable
917+
→ Worker works on the feature instead of doing nothing.
918+
"""
919+
agent = make_agent(
920+
tmp_path, name="worker", required_labels=["in-progress", "in-review", "bug"],
921+
github_api=mock_github_api,
922+
)
923+
monkeypatch.setattr(type(agent), "_bot_logins", property(lambda self: BOT_LOGINS))
924+
925+
mock_github_api.set_issues("in-progress", make_issue_list_items((43, "2024-01-01T00:00:00Z")))
926+
mock_github_api.set_issues("in-review", [])
927+
mock_github_api.set_issues("bug", make_issue_list_items((50, "2024-01-01T00:00:00Z")))
928+
mock_github_api.set_issues_detailed("in-progress", [
929+
make_issue_detail(
930+
number=43,
931+
title="Add user profiles",
932+
labels=["in-progress"],
933+
comments=[
934+
{
935+
"author": {"login": "alice"},
936+
"body": "Go ahead and implement this.",
937+
"createdAt": "2024-01-07T00:00:00Z",
938+
},
939+
],
940+
),
941+
])
942+
mock_github_api.set_issues_detailed("in-review", [])
943+
mock_github_api.set_issues_detailed("bug", [
944+
make_issue_detail(
945+
number=50,
946+
title="Search fails when query is empty",
947+
body="Crash on empty query",
948+
labels=["bug"],
949+
comments=[],
950+
),
951+
])
952+
mock_github_api.set_prs([
953+
make_pull_request(10, "issue-50-fix-empty-search"),
954+
])
955+
956+
calls = capture_popen(stdout_lines=[result_event()], returncode=0)
957+
958+
result = agent.run()
959+
960+
assert result.success is True
961+
prompt = extract_prompt(calls)
962+
assert "Add user profiles" in prompt
963+
assert "Issue #43" in prompt
964+
870965
def test_bug_prioritized_over_in_progress(
871966
self, tmp_path, mock_github_api, capture_popen, monkeypatch
872967
):

0 commit comments

Comments
 (0)