You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/09/28 06:39:59 UTC
(magpie) branch main updated: feat(pr-triage): recognise triage markers left by any triager, not only the viewer (#1420)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git
The following commit(s) were added to refs/heads/main by this push:
new 7eb89add feat(pr-triage): recognise triage markers left by any triager, not only the viewer (#1420)
7eb89add is described below
commit 7eb89adddb2f32efde6b74d78786c482ca225ed8
Author: kuse <31...@qq.com>
AuthorDate: Mon Sep 28 14:39:54 2026 +0800
feat(pr-triage): recognise triage markers left by any triager, not only the viewer (#1420)
* feat(pr-triage): recognise triage markers left by any triager, not only the viewer
Widen the already_triaged / first_time_stale_abandoned marker detection to
any comment whose authorAssociation is OWNER, MEMBER, or COLLABORATOR (the
same gate pr-management-stats already applies), carry the triager login in a
new optional by= field on the pr-triage-fold block, and surface it in the
rows 3-4 reason templates. Add eval fixtures for the cross-triager comment
channel, the non-triager negative case, and a legacy fold without by=.
Refs #76
Generated-by: ZCode (GLM-5.3-Flash)
* fix(pr-triage): gate the Sweep 1a close clock on the live committer check; reshape eval fixtures per review
A non-viewer comment-channel marker is load-bearing for Sweep 1a's close:
association alone must not start the close clock (read/triage collaborators
return COLLABORATOR too), so the marker author passes the live maintainer
test before last_triage_comment_at may be established from it. Widen the
last_maintainer_comment_at exclusion to any triager's marker comments so a
marker cannot double-count as maintainer activity.
Reshape the eval fixtures per review: case-20 becomes a non-draft PR with a
~4-day-old marker (escapes F5a's 72h window and F6, which filtered the
previous shape before row 3 could fire); case-22 replaces the case-17
duplicate with the actually-untested path -- a fold carrying by=<another
triager>, where the reason must name that triager. Reference fixtures by
full directory name in the family README.
Generated-by: ZCode (GLM-5.3-Flash)
---
.../skills/pr-triage/actions.md | 2 +-
.../skills/pr-triage/classify-and-act.md | 53 +++++++++++++++-------
.../skills/pr-triage/comment-templates.md | 12 ++++-
.../skills/pr-triage/rationale.md | 21 +++++++++
.../skills/pr-triage/stale-sweeps.md | 29 ++++++++++--
tools/skill-evals/README.md | 2 +-
.../evals/pr-management-triage/README.md | 13 +++++-
.../expected.json | 5 ++
.../report.md | 25 ++++++++++
.../expected.json | 5 ++
.../report.md | 25 ++++++++++
.../case-22-fold-by-another-triager/expected.json | 5 ++
.../case-22-fold-by-another-triager/report.md | 27 +++++++++++
.../decision-table/fixtures/system-prompt.md | 5 +-
14 files changed, 200 insertions(+), 29 deletions(-)
diff --git a/plugins/magpie-pr-management/skills/pr-triage/actions.md b/plugins/magpie-pr-management/skills/pr-triage/actions.md
index 19bc86cc..5228f4a2 100644
--- a/plugins/magpie-pr-management/skills/pr-triage/actions.md
+++ b/plugins/magpie-pr-management/skills/pr-triage/actions.md
@@ -71,7 +71,7 @@ The body is built from the `draft` template in
notification.** Render the body wrapped per
[`comment-templates.md#body-fold-rendering`](comment-templates.md#body-fold-rendering)
(no `@`-mention; `<ai_attribution_footer_body>`; opening marker
-carrying `triaged=<ISO-UTC> head=<sha7> action=draft`) into
+carrying `triaged=<ISO-UTC> head=<sha7> action=draft by=<viewer-login>`) into
`/tmp/pr-<N>-foldblock.md`, then read-modify-write the body:
```bash
diff --git a/plugins/magpie-pr-management/skills/pr-triage/classify-and-act.md b/plugins/magpie-pr-management/skills/pr-triage/classify-and-act.md
index 5cdae05d..c6e75baa 100644
--- a/plugins/magpie-pr-management/skills/pr-triage/classify-and-act.md
+++ b/plugins/magpie-pr-management/skills/pr-triage/classify-and-act.md
@@ -146,12 +146,12 @@ Action verbs are defined in [`actions.md`](actions.md).
| # | Precondition (all must hold) | Classification | Action | Reason template |
|----|-----------------------------------------------------------------------------------------------|---------------------------|------------------------|-----------------|
-| 0 | [`first_time_stale_abandoned`](#first_time_stale_abandoned) — first-time contributor PR with a prior viewer triage marker (comment marker **or** [`viewer_triage_fold_present`](#viewer_triage_fold_present)) and no commits since the marker, ≥ 30 days old | `first_time_stale_abandoned` | `skip` | First-time contributor's PR was triaged ≥ 30d ago, no push since — let the stale-sweep retire it rather than re-approving CI |
+| 0 | [`first_time_stale_abandoned`](#first_time_stale_abandoned) — first-time contributor PR with a prior triage marker by any triager (comment marker **or** [`viewer_triage_fold_present`](#viewer_triage_fold_present)) and no commits since the marker, ≥ 30 days old | `first_time_stale_abandoned` | `skip` | First-time contributor's PR was triaged ≥ 30d ago, no push since — let the stale-sweep retire it rather than re-approving CI |
| 1 | `head_sha` appears in the per-page `action_required` REST index, OR ([`first_time_no_real_ci`](#first_time_no_real_ci)) | `pending_workflow_approval` | `approve-workflow` | First-time contributor — review the diff and approve CI, or flag suspicious |
| 2 | [`copilot_review_stale`](#copilot_review_stale) | `stale_copilot_review` | `draft` (include the specific Copilot thread URL in the violation body) | Unaddressed Copilot review ≥ 7 days old — convert to draft |
-| 3 | Viewer triage marker present, after last commit, age < 7 days, sub-state `waiting`. **Marker = viewer comment with the `Pull Request quality criteria` link (comment channel) OR [`viewer_triage_fold_present`](#viewer_triage_fold_present) with matching `head` (pr-body channel).** | `already_triaged` | `skip` | Already triaged M days ago — still waiting on author |
-| 4 | Same as #3 (either channel) but sub-state `responded` | `already_triaged` | `skip` | Already triaged M days ago — author responded, maintainer to re-engage |
-| 5 | Viewer triage marker present (either channel — see row 3), after last commit, sub-state `waiting`, age ≥ 7 days, `isDraft == true` | `stale_draft` | (defer to [`stale-sweeps.md`](stale-sweeps.md) Sweep 1a) | Draft triaged N days ago, no author reply |
+| 3 | Triage marker by any triager present, after last commit, age < 7 days, sub-state `waiting`. **Marker = a comment by a triager (authorAssociation `OWNER`, `MEMBER`, or `COLLABORATOR`) with the `Pull Request quality criteria` link (comment channel) OR [`viewer_triage_fold_present`](#viewer_triage_fold_present) with matching `head` (pr-body channel).** | `already_triaged` | `skip` | Already triaged M days ago by `<triager>` — still waiting on author |
+| 4 | Same as #3 (either channel) but sub-state `responded` | `already_triaged` | `skip` | Already triaged M days ago by `<triager>` — author responded, maintainer to re-engage |
+| 5 | Triage marker by any triager present (either channel — see row 3), after last commit, sub-state `waiting`, age ≥ 7 days, `isDraft == true` | `stale_draft` | (defer to [`stale-sweeps.md`](stale-sweeps.md) Sweep 1a) | Draft triaged N days ago, no author reply |
| 6 | `viewer == pr.author.login` | n/a | `skip` | You are the PR author — triage skipped |
| 7a | `now - createdAt < 30min` | n/a | `skip` | Too fresh — CI still warming up |
| 7b | [`security_language_signal`](#security_language_signal) | `security_language_signal` | `comment` | Security-language in title / body / commits — ask contributor to neutralise or confirm CVE disclosure complete |
@@ -537,13 +537,27 @@ parse the opening marker's space-separated metadata:
`viewer_triage_fold_present` as **false** for the already-triaged
rows in that case).
- `action=<draft|comment|close>` — informational.
-
-**The "viewer triage marker exists, posted after last commit"
-precondition in rows 0, 3, 4, and 5 is satisfied by EITHER** a
-viewer comment containing the `Pull Request quality criteria`
-marker with `createdAt` after the head `committedDate` (the
-`comment` channel / legacy PRs) **OR** `viewer_triage_fold_present`
-with `head=` matching the current head (the `pr-body` channel).
+- `by=<login>` — the triager who wrote the block. Feeds the
+ `<triager>` substitution in the rows 3–4 reason templates.
+ Folds written before this field existed carry no `by=` — read
+ them without it and omit the `by <triager>` clause in the
+ reason (backward compatible; every fold written from
+ [`comment-templates.md#body-fold-rendering`](comment-templates.md#body-fold-rendering)
+ onward includes it).
+
+**The "triage marker by any triager exists, posted after last
+commit" precondition in rows 0, 3, 4, and 5 is satisfied by
+EITHER** a comment by a triager — authorAssociation `OWNER`,
+`MEMBER`, or `COLLABORATOR` — containing the
+`Pull Request quality criteria` marker with `createdAt` after
+the head `committedDate` (the `comment` channel / legacy PRs)
+**OR** `viewer_triage_fold_present` with `head=` matching the
+current head (the `pr-body` channel). The association gate keeps
+the PR author's own comments and bot comments (association
+`NONE`) from counting as triage markers. The field name
+`viewer_triage_fold_present` is historical: the fold block lives
+on the PR body and is shared across triagers, so any triager's
+fold counts, not just the viewer's.
The downstream age and sub-state logic is identical once the
"triaged-at" anchor is resolved (the comment's `createdAt`, or the
fold's `triaged=`).
@@ -677,8 +691,9 @@ All of:
- `authorAssociation == FIRST_TIME_CONTRIBUTOR` or
`FIRST_TIMER`.
-- A viewer triage marker exists, in **either channel**: a comment
- by the viewer in `comments(last:10)` containing the literal
+- A triage marker by any triager exists, in **either channel**: a
+ comment by a triager (authorAssociation `OWNER`, `MEMBER`, or
+ `COLLABORATOR`) in `comments(last:10)` containing the literal
string `Pull Request quality criteria` (comment channel), OR
[`viewer_triage_fold_present`](#viewer_triage_fold_present) in
the PR body (pr-body channel).
@@ -829,10 +844,14 @@ body. Rules:
- One line. Factual. Lead with the signal that fired the rule;
end with the proposal verb where applicable.
-- Substitute placeholders (`<base>`, `<reviewers>`, `N`, `K`,
- `M`) from the PR record. `<reviewers>` is `@login` mentions
+- Substitute placeholders (`<base>`, `<reviewers>`, `<triager>`,
+ `N`, `K`, `M`) from the PR record. `<reviewers>` is `@login` mentions
joined with a comma followed by a single space — see the canonical example
- below. Substitution happens at classification time;
+ below. `<triager>` is the login of the triager who left the
+ detected triage marker (the marker comment's `author.login`, or
+ the fold's `by=` value); when the fold carries no `by=` field,
+ omit the `by <triager>` clause instead of guessing.
+ Substitution happens at classification time;
[`interaction-loop.md`](interaction-loop.md) displays the
already-substituted string verbatim.
- Never editorialise. Never include emoji or scare quotes.
@@ -874,7 +893,7 @@ applies — rows do not get to reach back for more data.
| `copilot_review_stale` (row 2) | `reviewThreads.nodes.{isResolved,comments.nodes.{author.login,createdAt,url}}`, `comments(last:10).nodes.{author.login,createdAt}` |
| `has_deterministic_signal`, `ci_failures_only`, `unresolved_threads_only`, `unresolved_threads_only_likely_addressed` (rows 8–17) | `mergeable`, `statusCheckRollup.{state,contexts}`, `reviewThreads.nodes.{isResolved,comments(first:5).nodes.{author.login,authorAssociation,createdAt}}`, `updatedAt`, `comments(last:10).nodes.{author.login,authorAssociation,createdAt}`, `commits(last:1).nodes.commit.committedDate`, `author.login` |
| Row 18 (`stale_review`) | `latestReviews.nodes.{state,author.login,submittedAt}`, `commits(last:1).nodes.commit.committedDate`, `comments(last:10)`, `reviewThreads.nodes.comments(first:5).nodes.{author.login,createdAt}` |
-| Rows 3–5 (`already_triaged` / `stale_draft` from triage marker) | `comments(last:10).nodes.{author.login,bodyText,createdAt}`, viewer login, `commits(last:1).nodes.commit.{oid,committedDate}`, **`body`** (raw — for [`viewer_triage_fold_present`](#viewer_triage_fold_present), the pr-body channel) |
+| Rows 3–5 (`already_triaged` / `stale_draft` from triage marker) | `comments(last:10).nodes.{author.login,authorAssociation,bodyText,createdAt}`, viewer login, `commits(last:1).nodes.commit.{oid,committedDate}`, **`body`** (raw — for [`viewer_triage_fold_present`](#viewer_triage_fold_present), the pr-body channel) |
| Rows 19, 20 (`passing`) | `statusCheckRollup.state`, `statusCheckRollup.contexts`, `mergeable`, `reviewThreads.totalCount`, `labels` |
---
diff --git a/plugins/magpie-pr-management/skills/pr-triage/comment-templates.md b/plugins/magpie-pr-management/skills/pr-triage/comment-templates.md
index 12116f64..3fd0b35c 100644
--- a/plugins/magpie-pr-management/skills/pr-triage/comment-templates.md
+++ b/plugins/magpie-pr-management/skills/pr-triage/comment-templates.md
@@ -359,7 +359,7 @@ span, appended at the **end** of the PR body (the author's own
description is preserved above it, untouched):
```markdown
-<!-- pr-triage-fold: triaged=2026-06-11T14:22:00Z head=abc1234 action=draft -->
+<!-- pr-triage-fold: triaged=2026-06-11T14:22:00Z head=abc1234 action=draft by=maya-triager -->
<the rendered template body — same content the comment channel would post,
minus the @-mention; see the no-@-mention rule below>
@@ -368,7 +368,8 @@ description is preserved above it, untouched):
```
- **Opening marker metadata** (one HTML comment, space-separated
- `key=value` fields — all required):
+ `key=value` fields — all required except `by=` on folds written
+ before the field existed):
- `triaged=<ISO-8601 UTC>` — the moment the block was written.
This replaces a comment's `createdAt` for every downstream
age / "posted after last commit" check (see
@@ -380,6 +381,13 @@ description is preserved above it, untouched):
new state).
- `action=<draft|comment|close>` — which action wrote the block,
for human readability and stats attribution.
+ - `by=<login>` — the triager who wrote the block. Surfaces in
+ the rows 3–4 reason template via the `<triager>` placeholder
+ (see
+ [`classify-and-act.md#reason-template-rules`](classify-and-act.md#reason-template-rules)),
+ which matters once another triager's fold suppresses a
+ re-proposal. Required on every fold written from this spec
+ onward; readers must accept folds without it (legacy).
- **`pr-triage-fold` and `/pr-triage-fold` are the literal,
framework-fixed marker tokens.** They must appear byte-for-byte
identical everywhere they are written or searched
diff --git a/plugins/magpie-pr-management/skills/pr-triage/rationale.md b/plugins/magpie-pr-management/skills/pr-triage/rationale.md
index 816fd5bc..044e5609 100644
--- a/plugins/magpie-pr-management/skills/pr-triage/rationale.md
+++ b/plugins/magpie-pr-management/skills/pr-triage/rationale.md
@@ -307,6 +307,27 @@ detectable from a single comment scan. Do not paraphrase the
marker in [`comment-templates.md`](comment-templates.md); it is
load-bearing.
+Marker detection is **triager-scoped, not viewer-scoped**. With
+several committers triaging the same queue, a marker left by
+another triager must suppress the duplicate proposal exactly
+like the viewer's own — the wasted-attention and
+duplicate-comment harm this row exists to prevent does not care
+who triaged. On the comment channel the gate is the author
+association (`OWNER` / `MEMBER` / `COLLABORATOR`), which is the
+same gate the `pr-management-stats` classifier already applies
+when counting triage-marker comments, so the skill and the stats
+tool agree on what a triage marker is. On the fold channel the
+block is a single managed span on the PR body — inherently
+shared across triagers — and carries the writer's login in the
+`by=` field, which feeds the `<triager>` substitution in the
+rows 3–4 reason templates so the maintainer can see *who* to
+sync with before re-engaging. Association alone only ever
+drives the `skip` rows; when a non-viewer's marker feeds a
+close (Sweep 1a's stale-draft clock), its author must pass the
+live committer check first — the exact rule
+[Maintainer detection — committer, not `authorAssociation`](stale-sweeps.md#maintainer-detection--committer-not-authorassociation)
+already imposes on every load-bearing maintainer test.
+
---
## Row 6 — viewer is the PR author
diff --git a/plugins/magpie-pr-management/skills/pr-triage/stale-sweeps.md b/plugins/magpie-pr-management/skills/pr-triage/stale-sweeps.md
index 3ac656a2..19eae377 100644
--- a/plugins/magpie-pr-management/skills/pr-triage/stale-sweeps.md
+++ b/plugins/magpie-pr-management/skills/pr-triage/stale-sweeps.md
@@ -82,14 +82,30 @@ Each stale sweep needs these timestamps per PR:
batch query)
- `last_triage_comment_at` — the most recent triage-marker
timestamp from **either feedback channel**, if any: the
- `createdAt` of the most recent viewer comment containing the
- `Pull Request quality criteria` marker (comment channel), OR
- the `triaged=` timestamp parsed from the `pr-triage-fold` block
+ `createdAt` of the most recent comment by a triager
+ (authorAssociation `OWNER`, `MEMBER`, or `COLLABORATOR`)
+ containing the `Pull Request quality criteria` marker (comment
+ channel), OR the `triaged=` timestamp parsed from the
+ `pr-triage-fold` block
in the PR `body` (pr-body channel — the default; see
[`viewer_triage_fold_present`](classify-and-act.md#viewer_triage_fold_present)).
When both are present (a project that switched channels), take
the later of the two. Despite the legacy field name, this is
"last triaged at", not strictly a comment.
+
+ **Close gate.** This timestamp starts Sweep 1a's `close` — the
+ least reversible action the skill takes — so a comment-channel
+ marker left by anyone other than the viewer is load-bearing
+ for a close: confirm its author passes the live maintainer
+ test ([Maintainer detection — committer, not
+ `authorAssociation`](#maintainer-detection--committer-not-authorassociation))
+ before it may establish `last_triage_comment_at`.
+ Association is only the cheap first filter; a
+ `read`/`triage` collaborator's marker comment does not start
+ the close clock. The viewer's own marker needs no live check
+ (the operator is running the sweep), and the fold channel
+ needs none (a fold is written by the triage tool under the
+ maintainer's seat, never by an arbitrary comment author).
- `last_author_activity_at` — the max of three timestamps,
all already in the batch query:
1. the head commit's `committedDate` from `commits(last: 1)`
@@ -108,8 +124,11 @@ Each stale sweep needs these timestamps per PR:
- `last_maintainer_comment_at` — the `createdAt` of the most
recent comment in `comments(last: 10)` whose
`authorAssociation` is one of `COLLABORATOR`/`MEMBER`/`OWNER`,
- excluding the viewer's own triage comments (the
- `Pull Request quality criteria` marker disqualifies).
+ excluding triage-marker comments by **any** triager (a comment
+ carrying the `Pull Request quality criteria` marker
+ disqualifies, whoever wrote it — the marker is already counted
+ through `last_triage_comment_at` and must not double-count as
+ maintainer activity).
All inputs come from the same aliased query that drives
classification — no extra fetches. If a PR hasn't been triaged
diff --git a/tools/skill-evals/README.md b/tools/skill-evals/README.md
index 9058696b..b27de619 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** — 51 cases across 5 steps (pre-filter, decision-table, terminal-links, pagination-dedup, interaction-progress)
+- **pr-management-triage** — 54 cases across 5 steps (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 e56230a4..28b3a37f 100644
--- a/tools/skill-evals/evals/pr-management-triage/README.md
+++ b/tools/skill-evals/evals/pr-management-triage/README.md
@@ -5,12 +5,12 @@
Behavioral evals for the `pr-management-triage` skill.
-## Suites (51 cases total)
+## Suites (54 cases total)
| Suite | Step | Cases | What it covers |
|---|---|---|---|
| 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) | 19 | Rows 3/4 (already-triaged via body-fold block→skip), 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-read [...]
+| 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=<another triager>`→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 [...]
| 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 |
| 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 |
@@ -28,6 +28,15 @@ wording or confirm completed CVE disclosure.
Cases `case-11` … `case-20` cover the review sources, body/author checks,
time boundaries and feedback ordering for [#78](https://github.com/apache/magpie/issues/78).
`case-21` preserves F5b's detection of a ping answered by a review.
+The decision-table fixtures `case-20-comment-marker-by-other-maintainer`,
+`case-21-non-triager-qc-comment-not-a-marker`, and
+`case-22-fold-by-another-triager` pin the cross-triager marker contract from
+[#76](https://github.com/apache/magpie/issues/76): a marker comment by another
+triager suppresses the re-proposal, a non-triager's comment quoting the QC
+link does not count, and a fold carrying `by=<another triager>` classifies as
+already-triaged with the reason naming that triager. (These directory names
+are decision-table fixtures; `case-20`/`case-21` in the pre-filter bullets
+above are a different suite.)
## Run
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-20-comment-marker-by-other-maintainer/expected.json b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-20-comment-marker-by-other-maintainer/expected.json
new file mode 100644
index 00000000..2b7d1847
--- /dev/null
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-20-comment-marker-by-other-maintainer/expected.json
@@ -0,0 +1,5 @@
+{
+ "classification": "already_triaged",
+ "action": "skip",
+ "reason": "Row 3: a triage-marker comment by another triager (kaxil, MEMBER) was posted after the last commit 4 days ago and the author has not pushed since — already triaged by kaxil, skip rather than re-propose a near-duplicate."
+}
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-20-comment-marker-by-other-maintainer/report.md b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-20-comment-marker-by-other-maintainer/report.md
new file mode 100644
index 00000000..48aef1c5
--- /dev/null
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-20-comment-marker-by-other-maintainer/report.md
@@ -0,0 +1,25 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0 -->
+
+PR #18011
+Author: felix-contributor
+AuthorAssociation: CONTRIBUTOR
+StatusCheckRollup: FAILURE
+FailedChecks: ["mypy-core"]
+RecentMainFailures: []
+Mergeable: MERGEABLE
+UnresolvedThreads: 0
+IsDraft: false
+CommitsBehind: 3
+RealCIRan: true
+Labels: []
+Now: 2026-05-20T10:00:00Z
+HeadSHA: def5678
+
+TriageComment (by kaxil, MEMBER, createdAt: 2026-05-16T09:00:00Z — NOT the
+viewer's own comment; the viewer running this session is potiuk):
+ "Converting to draft — this PR doesn't yet meet our Pull Request quality
+ criteria. The type-checking failure needs a code fix; please mark it
+ Ready for review once addressed."
+
+LastAuthorActivity: 2026-05-15T08:00:00Z
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-21-non-triager-qc-comment-not-a-marker/expected.json b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-21-non-triager-qc-comment-not-a-marker/expected.json
new file mode 100644
index 00000000..19adb60d
--- /dev/null
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-21-non-triager-qc-comment-not-a-marker/expected.json
@@ -0,0 +1,5 @@
+{
+ "classification": "deterministic_flag",
+ "action": "comment",
+ "reason": "Row 12: the only failure is the static check mypy-core; the PR author's own comment quoting the quality-criteria link is not a triage marker (author association is FIRST_TIME_CONTRIBUTOR), so the already-triaged rows do not apply — suggest comment."
+}
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-21-non-triager-qc-comment-not-a-marker/report.md b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-21-non-triager-qc-comment-not-a-marker/report.md
new file mode 100644
index 00000000..914f5080
--- /dev/null
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-21-non-triager-qc-comment-not-a-marker/report.md
@@ -0,0 +1,25 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0 -->
+
+PR #18012
+Author: dana-contributor
+AuthorAssociation: FIRST_TIME_CONTRIBUTOR
+StatusCheckRollup: FAILURE
+FailedChecks: ["mypy-core"]
+RecentMainFailures: []
+Mergeable: MERGEABLE
+UnresolvedThreads: 0
+IsDraft: true
+CommitsBehind: 10
+RealCIRan: true
+Labels: []
+Now: 2026-05-20T10:00:00Z
+HeadSHA: aabb001
+
+Comments (last 10):
+ - author: dana-contributor (FIRST_TIME_CONTRIBUTOR), createdAt: 2026-05-19T12:00:00Z
+ body: "I rebased and I believe this now meets the Pull Request quality
+ criteria — could a maintainer take another look?"
+
+No fold block in the PR body. No comments by OWNER, MEMBER, or COLLABORATOR
+authors.
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-22-fold-by-another-triager/expected.json b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-22-fold-by-another-triager/expected.json
new file mode 100644
index 00000000..04c2b71b
--- /dev/null
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-22-fold-by-another-triager/expected.json
@@ -0,0 +1,5 @@
+{
+ "classification": "already_triaged",
+ "action": "skip",
+ "reason": "Row 3: the PR body carries a pr-triage-fold block written by kaxil-maintainer whose head (abc1234) matches the current head SHA and was triaged 2 days ago with no author activity since — already triaged by kaxil-maintainer, skip rather than re-flag the folded violation."
+}
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-22-fold-by-another-triager/report.md b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-22-fold-by-another-triager/report.md
new file mode 100644
index 00000000..39538a95
--- /dev/null
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/case-22-fold-by-another-triager/report.md
@@ -0,0 +1,27 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0 -->
+
+PR #18013
+Author: nora-contributor
+AuthorAssociation: FIRST_TIME_CONTRIBUTOR
+StatusCheckRollup: FAILURE
+FailedChecks: ["mypy-core"]
+RecentMainFailures: []
+Mergeable: MERGEABLE
+UnresolvedThreads: 0
+IsDraft: true
+CommitsBehind: 5
+RealCIRan: true
+Labels: []
+Now: 2026-05-18T10:00:00Z
+HeadSHA: abc1234
+
+PRBodyFoldBlock (pr-triage-fold; triaged=2026-05-16T09:00:00Z head=abc1234
+action=draft by=kaxil-maintainer — the fold was written by another triager,
+not the viewer running this session):
+ "Converting to draft — this PR doesn't yet meet our Pull Request quality
+ criteria.
+ - mypy (type checking): failing mypy-core. See the linked criteria, then
+ mark the PR Ready for review."
+
+LastAuthorActivity: 2026-05-15T08:00:00Z
diff --git a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/system-prompt.md b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/system-prompt.md
index e6e08347..74366a4e 100644
--- a/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/system-prompt.md
+++ b/tools/skill-evals/evals/pr-management-triage/decision-table/fixtures/system-prompt.md
@@ -46,7 +46,10 @@ and reason.
- **`viewer_triage_fold_present`** — the PR body contains a `pr-triage-fold`
managed block (the body-fold feedback channel, default
`triage_feedback_channel: pr-body`). The block's opening marker carries
- `triaged=<ISO>` (the triage timestamp) and `head=<sha7>`. The block counts as
+ `triaged=<ISO>` (the triage timestamp) and `head=<sha7>`, and folds written
+ under the current spec also carry `by=<login>` (the triager who wrote the
+ block; legacy folds may omit it — do not invent a triager name when it is
+ absent). The block counts as
an after-last-commit triage marker **only when `head=` equals the PR's current
`HeadSHA`** (the author has not pushed since the fold). If `head=` no longer
matches the current head, treat `viewer_triage_fold_present` as **false** — the