diff --git a/skills/nextwork/SKILL.md b/skills/nextwork/SKILL.md index 19f7b99e01..885f2f9b5d 100644 --- a/skills/nextwork/SKILL.md +++ b/skills/nextwork/SKILL.md @@ -228,10 +228,8 @@ like production dispatch: first whitespace token of the first comment line. itself is older than `--stale-hours` (default 6h); raw age still uses `--triage-stale-hours` (default 72h). - `waiting_ci` and `waiting_merge_queue` are not flipped by `--stale-hours`. -- Merge-queue membership is only checked for PRs labeled `ready-for-merge` - (to avoid an extra API call per PR); the check uses the PR's `baseRefName` - when available (not only the repo default branch). Other PRs never report - `in_merge_queue`. +- Merge-queue membership is checked for all open PRs; the check uses the + PR's `baseRefName` when available (not only the repo default branch). - Linked-PR detection scans open PRs only when an issue reaches that check (after blockers / assignment / sub-issues). The scan is capped at five GraphQL pages (~500 PRs) per repo; beyond that, some links may be missed. diff --git a/skills/nextwork/scripts/nextwork.py b/skills/nextwork/scripts/nextwork.py index 6ddfdf50bf..7bc82d0958 100644 --- a/skills/nextwork/scripts/nextwork.py +++ b/skills/nextwork/scripts/nextwork.py @@ -920,6 +920,14 @@ def classify_pr( eliminated=True, ) + # Merge-queue membership is authoritative regardless of labels. + if item.get("in_merge_queue"): + return Classification( + status="waiting_merge_queue", + reason="Already enqueued in the merge queue", + eliminated=True, + ) + inflight = classify_inflight_agent(comments, stale_hours, now) if inflight: return inflight @@ -980,12 +988,6 @@ def classify_pr( ) if "ready-for-merge" in labels: - if item.get("in_merge_queue"): - return Classification( - status="waiting_merge_queue", - reason="Already enqueued in the merge queue", - eliminated=True, - ) if checks_pending: return Classification( status="waiting_ci", @@ -1927,9 +1929,9 @@ def _resolve_linked(r=repo, n=number, fn=get_linked) -> list[int]: def maybe_check_merge_queue(items: list[dict[str, Any]], fetcher: MergeQueueChecker) -> None: - """Second pass: only hits the merge-queue API for PRs labeled ready-for-merge.""" + """Second pass: hits the merge-queue API for all open PRs.""" for item in items: - if item["kind"] == "pull" and "ready-for-merge" in item.get("labels", []): + if item["kind"] == "pull": item["in_merge_queue"] = fetcher.is_in_merge_queue( item["repo"], item["number"], @@ -2552,9 +2554,9 @@ def main(argv: list[str] | None = None) -> None: quiet=args.quiet, ) maybe_check_merge_queue(items, fetcher) - # Merge-queue membership can change ready_to_merge -> waiting_merge_queue; reclassify. + # Merge-queue membership can change any status -> waiting_merge_queue; reclassify. for item in items: - if item["kind"] == "pull" and "ready-for-merge" in item.get("labels", []): + if item["kind"] == "pull" and item.get("in_merge_queue"): classification = classify_item( item, user, diff --git a/skills/nextwork/scripts/nextwork_test.py b/skills/nextwork/scripts/nextwork_test.py index f43913e2bd..368878f46c 100644 --- a/skills/nextwork/scripts/nextwork_test.py +++ b/skills/nextwork/scripts/nextwork_test.py @@ -1174,6 +1174,20 @@ def test_waiting_merge_queue(self): self.assertEqual(result.status, "waiting_merge_queue") self.assertTrue(result.eliminated) + def test_waiting_merge_queue_without_ready_for_merge_label(self): + """Merge-queue detection works regardless of which labels are present.""" + item = make_pr(labels=["requires-manual-review"], in_merge_queue=True) + result = classify_pr(item, "alice", 6, NOW) + self.assertEqual(result.status, "waiting_merge_queue") + self.assertTrue(result.eliminated) + + def test_waiting_merge_queue_no_labels(self): + """Merge-queue detection works even with no control labels at all.""" + item = make_pr(labels=[], in_merge_queue=True) + result = classify_pr(item, "alice", 6, NOW) + self.assertEqual(result.status, "waiting_merge_queue") + self.assertTrue(result.eliminated) + def test_waiting_fix_bot_author(self): item = make_pr( author="fullsend-ai-coder[bot]", @@ -2212,15 +2226,18 @@ def test_apply_with_confirmed_ok(self): class TestMaybeCheckMergeQueue(unittest.TestCase): - def test_sets_flag_only_for_ready_for_merge_prs(self): + def test_checks_all_prs_regardless_of_labels(self): class StubFetcher: + def __init__(self): + self.calls = [] + def is_in_merge_queue(self, repo, number, *, base_branch=None): - self.seen = (repo, number, base_branch) - return number == 2 + self.calls.append((repo, number, base_branch)) + return number == 1 stub = StubFetcher() items = [ - {"kind": "pull", "repo": "a/b", "number": 1, "labels": ["ready-for-review"]}, + {"kind": "pull", "repo": "a/b", "number": 1, "labels": ["requires-manual-review"]}, { "kind": "pull", "repo": "a/b", @@ -2231,10 +2248,14 @@ def is_in_merge_queue(self, repo, number, *, base_branch=None): {"kind": "issue", "repo": "a/b", "number": 3, "labels": ["ready-for-merge"]}, ] maybe_check_merge_queue(items, stub) - self.assertNotIn("in_merge_queue", items[0]) - self.assertTrue(items[1]["in_merge_queue"]) - self.assertEqual(stub.seen, ("a/b", 2, "release")) + self.assertTrue(items[0]["in_merge_queue"]) + self.assertFalse(items[1]["in_merge_queue"]) + # Issues are skipped. self.assertNotIn("in_merge_queue", items[2]) + # Both PRs checked (regardless of labels), issue skipped. + self.assertEqual(len(stub.calls), 2) + self.assertIn(("a/b", 1, None), stub.calls) + self.assertIn(("a/b", 2, "release"), stub.calls) class TestFetchErrors(unittest.TestCase):