diff --git a/plugins/magpie-pr-management/skills/pr-triage/SKILL.md b/plugins/magpie-pr-management/skills/pr-triage/SKILL.md index 7025a4213..eae48c216 100644 --- a/plugins/magpie-pr-management/skills/pr-triage/SKILL.md +++ b/plugins/magpie-pr-management/skills/pr-triage/SKILL.md @@ -27,7 +27,7 @@ argument-hint: "[pr:N] [label:LBL] [author:LOGIN] [review-for-me] [stale] [repo: capability: capability:triage surface_hash: sha256:5c92df54aab39ad7 license: Apache-2.0 -measured_tokens: 5005 +measured_tokens: 5027 --- @@ -316,7 +316,7 @@ Selector semantics (`triage pr:` / `label:` / `author:` / `review --- **Step 0.7 — backport check:** only when `backport_branches` is configured — for every open PR targeting a release branch (any author, drafts included), verify it is a direct cherry-pick of a default-branch commit and, under `backport_policy: fixes-only`, that the source change is a fix rather than a feature, behaviour change, deprecation, removal or refactor; the spec is in [`backport-check.md`](backport-check.md). -**Step 1 — fetch:** resolve the selector per [`fetch-and-batch.md#inputs`](fetch-and-batch.md#inputs), walk every page of the aliased PR-list query until `pageInfo.hasNextPage` is false, deduplicate at the end, and prefetch the `action_required` run index and the recent main-branch failures once per session — the canonical loop is in [`fetch-and-batch.md#full-pagination-loop`](fetch-and-batch.md#full-pagination-loop). +**Step 1 — fetch:** resolve the selector per [`fetch-and-batch.md#inputs`](fetch-and-batch.md#inputs), walk every page of the aliased PR-list query until `pageInfo.hasNextPage` is false, deduplicate at the end, silently suppress PRs the session cache already holds under a terminal `action_taken` with an unchanged head SHA, and prefetch the `action_required` run index and the recent main-branch failures once per session — the canonical loop is in [`fetch-and-batch.md#full-pagination-loop`](fetch-and-batch.md#full-pagination-loop). **Step 2 — classify:** run **every PR fetched in Step 1** through [`classify-and-act.md`](classify-and-act.md), once — the pre-filters diff --git a/plugins/magpie-pr-management/skills/pr-triage/fetch-and-batch.md b/plugins/magpie-pr-management/skills/pr-triage/fetch-and-batch.md index db66ce309..26a6ae9ce 100644 --- a/plugins/magpie-pr-management/skills/pr-triage/fetch-and-batch.md +++ b/plugins/magpie-pr-management/skills/pr-triage/fetch-and-batch.md @@ -326,7 +326,10 @@ returns, for every PR on the page: Accumulate every PR into a single in-memory list, then deduplicate it by number after the final page, keeping each PR's last (freshest) -occurrence. Do not classify, do not present, do not prompt the +occurrence, then silently suppress PRs the session cache already +holds under a terminal `action_taken` with an unchanged head SHA (see +[`#full-pagination-loop`](#full-pagination-loop)). Do not classify, +do not present, do not prompt the maintainer between pages — the fetch loop is uninterrupted, runs to completion, and emits one progress line per page so the maintainer can step away during the wait. See @@ -355,6 +358,8 @@ loop serially until `pageInfo.hasNextPage` is false, then hand the accumulated list to Step 2. ```text +cache = load_session_cache().prs # may be empty — a fresh session starts empty + all_prs = [] cursor = null page = 1 @@ -374,7 +379,18 @@ for pr in reverse(all_prs): continue seen_numbers.add(pr.number) deduped_prs_reversed.append(pr) -return reverse(deduped_prs_reversed) +queue = reverse(deduped_prs_reversed) + +# Session suppression: a PR already taken to a terminal action earlier +# in this session, whose head SHA is unchanged, has nothing new to +# read. Drop it silently — no queue entry, no progress line, no prompt. +kept = [] +for pr in queue: + entry = cache.get(pr.number) + if entry and entry.get("action_taken") and entry.get("head_sha") == pr.head_sha: + continue + kept.append(pr) +return kept ``` Key invariants: @@ -398,6 +414,19 @@ Key invariants: first record for each PR number, then restore the list order. This keeps the last (freshest) occurrence and its position in the fetched ordering. Leave an already-unique list unchanged. +- **Suppress already-acted-on, unchanged-head entries.** After + deduplication, drop every PR the session cache holds under a + terminal `action_taken` whose cached `head_sha` equals the + freshly fetched head SHA — the decision was already made and + nothing has changed since, so re-surfacing the PR only invites + a second, possibly different decision. The drop is silent: the + PR must not appear in any group, progress line, or the Step 6 + summary. A PR whose head SHA **differs** does not match the + suppression and is re-classified as usual — the invalidation + rule in [`#session-cache`](#session-cache) and this suppression + are complementary, not conflicting. The suppression is session + state: it dies with the cache, so a fresh session re-surfaces + everything. - **Skip prefetch heuristics.** With pages fetched serially up front, there is no per-page maintainer wait to overlap with a next-page prefetch — the old prefetch-during-interaction @@ -686,6 +715,15 @@ anything that isn't needed. Schema: - An entry's `head_sha` must match the head SHA returned by the current fetch — if it doesn't, the contributor pushed since and the entry is stale. Drop it and re-classify. +- Conversely, an entry carrying a terminal `action_taken` whose + `head_sha` **does** match the current fetch suppresses the PR + for the rest of the session: Step 1 drops it silently before + handing the queue to Step 2 (see + [`#full-pagination-loop`](#full-pagination-loop)). Unchanged + head plus already-acted-on means nothing new to read; a changed + head falls through to the staleness rule above, so the two + rules never fire against each other. The suppression dies with + the session cache — a fresh session re-surfaces everything. - The `recent_main_failures` block is valid for 4 hours; after that, re-fetch via the canary/main-branch failure query (see below). diff --git a/tools/skill-evals/README.md b/tools/skill-evals/README.md index 5b7fc035c..e2881e1ba 100644 --- a/tools/skill-evals/README.md +++ b/tools/skill-evals/README.md @@ -33,7 +33,7 @@ Suites are currently implemented for: - **pr-management-code-review**: 126 cases across 28 suites (selector-resolution, step-1-selectors-match-chips, step-2-reviewer-resolution, step-2.5-slop-detection, step-3-security-disclosure-scan, step-3-ai-authorship-disclosure, step-4-* checks, step-5-adversarial-integration, step-6-disposition, step-7b-review-body-attribution, review-risk-classify, injection-guard, review-disposition, review-handoff) - **pr-management-mentor** — 29 cases across 3 steps (tone-checks, hand-off) - **pr-management-stats** — 13 cases across 2 steps (classify, pressure-weight) -- **pr-management-triage** — 59 cases across 6 steps (backport-check, pre-filter, decision-table, terminal-links, pagination-dedup, interaction-progress) +- **pr-management-triage** — 62 cases across 6 steps (backport-check, pre-filter, decision-table, terminal-links, pagination-dedup, interaction-progress) - **list-skills** — 8 cases across 2 steps (step-1-command, step-2-present) - **setup-isolated-setup-verify** — 17 cases across 3 steps (runtime-routing, step-1-classify, step-2-recommend) - **setup-isolated-setup-update** — 15 cases across 4 steps (runtime-routing, step-snapshot-drift, step-tool-freshness, step-after-report) diff --git a/tools/skill-evals/evals/pr-management-triage/README.md b/tools/skill-evals/evals/pr-management-triage/README.md index 30bd841d2..ddf3f01ea 100644 --- a/tools/skill-evals/evals/pr-management-triage/README.md +++ b/tools/skill-evals/evals/pr-management-triage/README.md @@ -5,7 +5,7 @@ Behavioral evals for the `pr-management-triage` skill. -## Suites (59 cases total) +## Suites (62 cases total) | Suite | Step | Cases | What it covers | |---|---|---|---| @@ -13,7 +13,7 @@ Behavioral evals for the `pr-management-triage` skill. | pre-filter | Step 2 (pre-filters) | 21 | F1 (collaborator), F2 (bot), F3 (draft recent), F4 (already ready), F5a (active maintainer feedback — general comments, review-thread comments, **and submitted top-level reviews with a non-empty body**, including the 72-hour and last-commit boundaries), F5b (maintainer ping unanswered; a ping answered by a review does not fire), F6 (maintainer co-drafted), row-6 (viewer is author), row-7a (fresh PR); clean contributor continues | | decision-table | Step 2 (decision table) | 22 | Rows 3/4 (already-triaged via a cross-triager comment marker→skip, via body-fold block→skip, and via a fold carrying `by=`→skip with the reason naming that triager), plus the negative case (a non-triager comment quoting the QC link is not a marker), 7b (security signal), 9 (conflict→draft), 10 (all systemic→rerun), 11 (partial systemic→rerun), 12 (static-only→comment), 13 (flaky ≤2→rerun), 14a (author confirmed→mark-ready), 14b (pending confirmation→skip), 14c (threads addressed→request-author-confirmation), 15 (threads→ping), 16 (no CI→rebase), 18 (changes-requested+new-commits→ping), 19 (already ready→skip), 20 (passing→mark-ready), 21 (stale draft sweep→close), 22 (rollup anomaly→skip) | | terminal-links | Golden rule 10 | 5 | Short, context-short, and full PR references retain their visible form and use the canonical target; `NO_COLOR` (including an empty value) and `TERM=dumb` select the plain-text fallback | -| pagination-dedup | Step 1 (full pagination) | 2 | A PR that moves to a later page is emitted once at its freshest occurrence; distinct PRs keep their fetched order | +| pagination-dedup | Step 1 (full pagination) | 5 | A PR that moves to a later page is emitted once at its freshest occurrence; distinct PRs keep their fetched order; a PR already acted on earlier in the session is silently suppressed while its head SHA is unchanged, re-classified once it moved, and never suppressed from a cache entry that carries no terminal `action_taken` | | interaction-progress | Interaction loop | 4 | Per-PR drill-ins retain the original one-based group position at the first, middle, and last rows and show the active classify-to-propose transition for `[E]` and `[P]` entry | The two body-fold cases (`case-17-fold-already-triaged`, diff --git a/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-3-acted-unchanged-head-suppressed/expected.json b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-3-acted-unchanged-head-suppressed/expected.json new file mode 100644 index 000000000..13384cf62 --- /dev/null +++ b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-3-acted-unchanged-head-suppressed/expected.json @@ -0,0 +1,5 @@ +{ + "prs": [ + {"number": 42, "updatedAt": "2026-08-05T12:05:00Z", "headRefOid": "head-42"} + ] +} diff --git a/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-3-acted-unchanged-head-suppressed/report.md b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-3-acted-unchanged-head-suppressed/report.md new file mode 100644 index 000000000..b76f81817 --- /dev/null +++ b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-3-acted-unchanged-head-suppressed/report.md @@ -0,0 +1,31 @@ + + +The search is sorted by `updated-asc`. These pages were fetched serially: + +Page 1: + +```json +[ + {"number": 41, "updatedAt": "2026-08-05T12:00:00Z", "headRefOid": "head-41"}, + {"number": 42, "updatedAt": "2026-08-05T12:05:00Z", "headRefOid": "head-42"} +] +``` + +Page 1 reports `hasNextPage: false`. + +The session cache for this repository holds: + +```json +{ + "prs": { + "41": { + "head_sha": "head-41", + "classification": "deterministic_flag", + "suggested_action": "comment", + "action_taken": "comment", + "action_at": "2026-08-05T09:30:00Z" + } + } +} +``` diff --git a/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-4-acted-pushed-head-reclassified/expected.json b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-4-acted-pushed-head-reclassified/expected.json new file mode 100644 index 000000000..e75e13141 --- /dev/null +++ b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-4-acted-pushed-head-reclassified/expected.json @@ -0,0 +1,6 @@ +{ + "prs": [ + {"number": 51, "updatedAt": "2026-08-05T12:10:00Z", "headRefOid": "fresh-51"}, + {"number": 52, "updatedAt": "2026-08-05T12:15:00Z", "headRefOid": "head-52"} + ] +} diff --git a/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-4-acted-pushed-head-reclassified/report.md b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-4-acted-pushed-head-reclassified/report.md new file mode 100644 index 000000000..a0fbb861a --- /dev/null +++ b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-4-acted-pushed-head-reclassified/report.md @@ -0,0 +1,33 @@ + + +The search is sorted by `updated-asc`. These pages were fetched serially: + +Page 1: + +```json +[ + {"number": 51, "updatedAt": "2026-08-05T12:10:00Z", "headRefOid": "fresh-51"}, + {"number": 52, "updatedAt": "2026-08-05T12:15:00Z", "headRefOid": "head-52"} +] +``` + +Page 1 reports `hasNextPage: false`. PR 51 was acted on earlier in this +session, but its head SHA differs from the cached one — the contributor +pushed since. + +The session cache for this repository holds: + +```json +{ + "prs": { + "51": { + "head_sha": "old-51", + "classification": "deterministic_flag", + "suggested_action": "rebase", + "action_taken": "rebase", + "action_at": "2026-08-05T09:45:00Z" + } + } +} +``` diff --git a/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-5-classified-without-action-stays/expected.json b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-5-classified-without-action-stays/expected.json new file mode 100644 index 000000000..7d97b5990 --- /dev/null +++ b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-5-classified-without-action-stays/expected.json @@ -0,0 +1,6 @@ +{ + "prs": [ + {"number": 61, "updatedAt": "2026-08-05T12:20:00Z", "headRefOid": "head-61"}, + {"number": 62, "updatedAt": "2026-08-05T12:25:00Z", "headRefOid": "head-62"} + ] +} diff --git a/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-5-classified-without-action-stays/report.md b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-5-classified-without-action-stays/report.md new file mode 100644 index 000000000..f6d35c2e9 --- /dev/null +++ b/tools/skill-evals/evals/pr-management-triage/pagination-dedup/fixtures/case-5-classified-without-action-stays/report.md @@ -0,0 +1,31 @@ + + +The search is sorted by `updated-asc`. These pages were fetched serially: + +Page 1: + +```json +[ + {"number": 61, "updatedAt": "2026-08-05T12:20:00Z", "headRefOid": "head-61"}, + {"number": 62, "updatedAt": "2026-08-05T12:25:00Z", "headRefOid": "head-62"} +] +``` + +Page 1 reports `hasNextPage: false`. PR 61 was classified earlier in this +session but no action was taken on it — the cache entry has no terminal +`action_taken`. + +The session cache for this repository holds: + +```json +{ + "prs": { + "61": { + "head_sha": "head-61", + "classification": "deterministic_flag", + "suggested_action": "draft" + } + } +} +```