Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/skills/pr-attention-queue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ A recorded response does not mean the discussion is resolved. `no-response` is o
bounded evidence is complete and there were zero top-level human responses. If the evidence is
incomplete, truncated, or requires human interpretation because of unresolved inline discussion, the
result remains `unknown` rather than `no-response`.
Do not count automation comments, including `dotnet-policy-service`, as human responses; complete
bot-only discussion evidence can establish `no-response`.
A non-author human coordination command is recorded response evidence because it demonstrates
engagement, but it does not prove resolution and does not create a discussion-verification signal by
itself. Coordination classification applies only to top-level discussion comments, never submitted
Expand Down Expand Up @@ -275,7 +277,10 @@ Do not promote a PR from `NeedsRescue`, `WaitingOnAuthor`, `WaitingOnCI`, or `De
Classification precedence is evidence-driven:

- An exact `* NO MERGE *` label requires maintainer triage even when CI is also pending.
- `pending-ci-rerun` routes to `WaitingOnCI`.
- Treat `pending-ci-rerun` as informational for review routing: it is an inactivity marker requiring
CI revalidation before merge, not evidence that CI is running. Preserve `ci-rerun-pending` on every
labeled classification, including drafts and excluded bot-authored PRs. Hold an otherwise
merge-ready PR in `WaitingOnCI` until CI is rerun and the label is removed.
- An approved PR whose merge state is `BEHIND` routes to author/maintainer branch-update work rather
than CI.
- A current non-author `COMMENTED` review routes to `WaitingOnAuthor` unless the author responded or
Expand Down
1 change: 1 addition & 0 deletions .github/skills/pr-attention-queue/presets.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependabot",
"dotnet-maestro",
"dotnet-maestro-bot",
"dotnet-policy-service",
"github-actions",
"copilot-pull-request-reviewer",
"azure-pipelines"
Expand Down
23 changes: 15 additions & 8 deletions .github/skills/pr-attention-queue/scripts/PRAttentionQueue.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -2433,12 +2433,6 @@ function Get-Classification {
$reasonCodes.Add("merge-conflict")
$blockers.Add("The pull request conflicts with its base branch.")
}
elseif ($ciRerunPending) {
$bucket = "WaitingOnCI"
$nextActor = "CI/automation"
$reasonCodes.Add("ci-rerun-pending")
$blockers.Add("The pull request is explicitly waiting for CI to be rerun.")
}
elseif ($reviewDecision -eq "CHANGES_REQUESTED" -or
($ownershipReview -and $ownershipReview.State -eq "CHANGES_REQUESTED")) {
if ($authorRespondedAfterReview) {
Expand Down Expand Up @@ -2585,6 +2579,17 @@ function Get-Classification {
$waitingSince = $ownershipReview.SubmittedAt
}

if ($ciRerunPending) {
if (-not $reasonCodes.Contains("ci-rerun-pending")) {
$reasonCodes.Add("ci-rerun-pending")
}
if ($bucket -eq "ReadyToMerge") {
$bucket = "WaitingOnCI"
$nextActor = "CI/automation"
$blockers.Add("CI must be rerun before merge.")
}
}

if ($bucket -eq "ReviewNow" -and $checkState -eq "Pending") {
$reasonCodes.Add("ci-pending")
}
Expand Down Expand Up @@ -2684,7 +2689,7 @@ function Get-DisplayMetadata {
}
"ci-rerun-pending" = [pscustomobject]@{
label = "CI rerun pending"
description = "The repository explicitly marks the pull request as waiting for a CI rerun."
description = "An inactivity marker requires CI revalidation before merge; it does not mean CI is running."
}
"community-contribution" = [pscustomobject]@{
label = "Community contribution"
Expand Down Expand Up @@ -2926,11 +2931,13 @@ function Get-ResponseEvidence {
$commentEvidenceTruncated -or
-not $complete -or
$signals.Count -gt 0
$onlyAutomationComments = $commentTotalCount -eq $comments.Count -and
@($comments | Where-Object { $_.Actor -ne "automation" }).Count -eq 0

$status = if ($recordedResponse) {
"recorded-response"
}
elseif ($complete -and $commentTotalCount -eq 0 -and -not $hasIncompleteOrAmbiguousEvidence) {
elseif ($complete -and $onlyAutomationComments -and -not $hasIncompleteOrAmbiguousEvidence) {
"no-response"
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ $fixturePath = Join-Path $PSScriptRoot "fixtures\merge-state-refresh.json"
$results = [Collections.Generic.List[object]]::new()
$cases = @(
@{ name = "clear"; kind = "regression"; eligibility = "eligible"; retries = 1; bucket = "ReadyToMerge"; reason = "approved"; actor = "merger" },
@{ name = "pending-ci-rerun"; kind = "regression"; retries = 1; bucket = "WaitingOnCI"; reason = "ci-rerun-pending"; actor = "CI/automation" },
@{ name = "unresolved-thread"; kind = "regression"; eligibility = "verification-needed"; retries = 1; bucket = "ReadyToMerge"; reason = "approved"; actor = "merger" },
@{ name = "unknown-state"; kind = "control"; retries = 1; reason = "merge-state-not-clean" },
@{ name = "missing-state"; kind = "control"; retries = 1; reason = "merge-state-not-clean" },
Expand Down Expand Up @@ -42,6 +43,7 @@ if ($Case) {
foreach ($scenario in $cases) {
$fixture = Get-Content -Raw -LiteralPath $fixturePath | ConvertFrom-Json -Depth 30
switch ($scenario.name) {
"pending-ci-rerun" { $fixture.labels += [pscustomobject]@{ name = "pending-ci-rerun" } }
"unresolved-thread" { $fixture.threads = @(@{ isResolved = $false; isOutdated = $false }) }
"unknown-state" { $fixture.retryResponse.mergeStateStatus = "UNKNOWN" }
"missing-state" { $fixture.retryResponse.PSObject.Properties.Remove("mergeStateStatus") }
Expand Down Expand Up @@ -99,6 +101,12 @@ foreach ($scenario in $cases) {
throw "Incomplete or blocked merge facts must not grant merge clearance."
}

if ($scenario.name -eq "pending-ci-rerun" -and
($item.mergeStateStatus -ne "CLEAN" -or $item.blockers -notcontains "CI must be rerun before merge." -or
$item.reasonCodes -notcontains "approved")) {
throw "Refreshed CLEAN state must preserve approval and the CI rerun merge gate."
}

$expectedBucket = $scenario.bucket ?? "WaitingOnCI"
$expectedActor = $scenario.actor ?? "CI/automation"
if ($item.bucket -ne $expectedBucket -or $item.nextActor -ne $expectedActor -or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ $fixturePath = Join-Path $PSScriptRoot "fixtures/pull-requests.json"
$correctnessFixturePath = Join-Path $PSScriptRoot "fixtures/correctness-pull-requests.json"
$discussionFixturePath = Join-Path $PSScriptRoot "fixtures/discussion-pull-requests.json"
$inboxFixturePath = Join-Path $PSScriptRoot "fixtures/inbox-pull-requests.json"
$rerunFixturePath = Join-Path $PSScriptRoot "fixtures/rerun-label-pull-requests.json"
$policyBotFixturePath = Join-Path $PSScriptRoot "fixtures/policy-bot-pull-requests.json"
$snapshot = [datetime]"2026-09-03T18:00:00Z"

Import-Module -Scope Local -Force $modulePath
Expand Down Expand Up @@ -126,6 +128,12 @@ Assert-True (($correctnessResult.items | Where-Object number -eq 101).bucket -eq
Assert-True (($correctnessResult.items | Where-Object number -eq 101).reasonCodes -contains "blocked-label") "The no-merge result must identify the blocking label."
Assert-True (($correctnessResult.items | Where-Object number -eq 102).bucket -eq "WaitingOnCI") "A pending CI rerun must not be ready to merge."
Assert-True (($correctnessResult.items | Where-Object number -eq 102).reasonCodes -contains "ci-rerun-pending") "The pending rerun must have a stable reason."
Assert-True (($correctnessResult.items | Where-Object number -eq 102).blockers -contains "CI must be rerun before merge.") "A merge-ready PR with a rerun label must explain the merge gate."
Assert-True (($correctnessResult.items | Where-Object number -eq 102).reasonCodes -contains "approved") "A rerun merge gate must preserve the otherwise-ready approval reason."
Assert-True (($correctnessResult.items | Where-Object number -eq 102).nextActor -eq "CI/automation") "A rerun merge gate must assign CI/automation."
Assert-True (($correctnessResult.items | Where-Object number -eq 102).mergeEligibility -eq "not-candidate") "A rerun merge gate must prevent merge candidacy."
Assert-True (-not ($correctnessResult.items | Where-Object number -eq 102).shownInMergeVerification) "A rerun merge gate must not enter merge verification."
Assert-True (($correctnessResult.items | Where-Object number -eq 101).reasonCodes -contains "ci-rerun-pending") "A no-merge label must preserve the informational rerun reason."
Assert-True (($correctnessResult.items | Where-Object number -eq 103).bucket -eq "WaitingOnCI") "A non-clean merge state must not be ready to merge."
Assert-True (($correctnessResult.items | Where-Object number -eq 103).reasonCodes -contains "merge-state-not-clean") "The non-clean merge state must have a stable reason."
Assert-True (($correctnessResult.items | Where-Object number -eq 104).bucket -eq "ReadyToMerge") "An approved clean pull request should be ready to merge."
Expand All @@ -136,6 +144,56 @@ Assert-True (($correctnessResult.items | Where-Object number -eq 106).bucket -eq
Assert-True (($correctnessResult.items | Where-Object number -eq 107).humanReviewCount -eq 0) "Author-authored reviews must not count as human reviewer activity."
Assert-True (($correctnessResult.items | Where-Object number -eq 107).bucket -eq "NeedsRescue") "A stale request must remain rescue work when the only review is author-authored."

$rerunJson = & $scriptPath `
-InputPath $rerunFixturePath `
-Now $snapshot `
-OutputFormat Json
$rerunResult = $rerunJson | ConvertFrom-Json -Depth 100
$rerunCases = @(
[pscustomobject]@{ Number = 301; Bucket = "ReviewNow"; Reason = "review-requested"; Actor = "human reviewer" },
[pscustomobject]@{ Number = 302; Bucket = "ReviewNow"; Reason = "needs-first-review"; Actor = "human reviewer" },
[pscustomobject]@{ Number = 303; Bucket = "WaitingOnAuthor"; Reason = "changes-requested"; Actor = "author" },
[pscustomobject]@{ Number = 304; Bucket = "WaitingOnCI"; Reason = "ci-failed"; Actor = "author/CI investigation" },
[pscustomobject]@{ Number = 305; Bucket = "Draft"; Reason = "draft"; Actor = "author" },
[pscustomobject]@{ Number = 306; Bucket = "WaitingOnAuthor"; Reason = "merge-conflict"; Actor = "author" },
[pscustomobject]@{ Number = 307; Bucket = "WaitingOnAuthor"; Reason = "branch-update-required"; Actor = "author/maintainer" },
[pscustomobject]@{ Number = 308; Bucket = "NeedsRescue"; Reason = "never-reviewed"; Actor = "maintainer/triager" },
[pscustomobject]@{ Number = 309; Bucket = "Excluded"; Reason = "bot-authored"; Actor = "none" },
[pscustomobject]@{ Number = 310; Bucket = "DesignDecision"; Reason = "design-gate"; Actor = "API/design owner" }
)
foreach ($case in $rerunCases) {
$item = $rerunResult.items | Where-Object number -eq $case.Number
Assert-True ($item.bucket -eq $case.Bucket) "Rerun label PR $($case.Number) must retain bucket '$($case.Bucket)'."
Assert-True ($item.nextActor -eq $case.Actor) "Rerun label PR $($case.Number) must retain next actor '$($case.Actor)'."
Assert-True ($item.reasonCodes -contains $case.Reason) "Rerun label PR $($case.Number) must retain normal reason '$($case.Reason)'."
Assert-True (@($item.reasonCodes | Where-Object { $_ -eq "ci-rerun-pending" }).Count -eq 1) "Rerun label PR $($case.Number) must have exactly one informational rerun reason."
Assert-True (-not ($item.blockers -contains "CI must be rerun before merge.")) "Rerun label PR $($case.Number) must not acquire a merge-ready rerun blocker."
Assert-True ($item.mergeEligibility -eq "not-candidate") "Rerun label PR $($case.Number) must remain outside merge candidacy."
}
Assert-True (($rerunResult.items | Where-Object number -eq 304).blockers -contains "The failure is not classified as unrelated or flaky.") "A failed-CI rerun PR must retain the normal CI failure blocker."
Assert-True (($rerunResult.items | Where-Object number -eq 302).reasonCodes -contains "ci-pending") "A first-review rerun PR must retain the informational pending-check reason."

$policyBotJson = & $scriptPath `
-InputPath $policyBotFixturePath `
-Now $snapshot `
-OutputFormat Json
$policyBotResult = $policyBotJson | ConvertFrom-Json -Depth 100
$policyBotItem = $policyBotResult.items | Where-Object number -eq 401
Assert-True ($policyBotItem.discussionAssessment.state -eq "clear") "A policy-bot-only discussion must be clear."
Assert-True (-not ($policyBotItem.discussionAssessment.signals -contains "non-author-discussion-requires-verification")) "A policy-bot-only discussion must not require non-author verification."
Assert-True ($policyBotItem.discussionAssessment.comments[0].actor -eq "automation") "The policy bot's bare login must be attributed to automation."
Assert-True ($policyBotItem.shownInDigest) "A policy-bot-only discussion must remain in the review digest."
$policyBotCommunity = $policyBotResult.inbox.community.inventory | Where-Object number -eq 401
Assert-True ($policyBotCommunity.responseEvidence.complete) "A policy-bot-only inbox fixture must have complete response evidence."
Assert-True ($policyBotCommunity.responseEvidence.status -eq "no-response") "A policy-bot-only community contribution must have no-response evidence."
Assert-True (-not $policyBotCommunity.responseEvidence.recordedNonAuthorHumanResponse) "A policy bot must not count as a recorded human response."
$policyMentionItem = $policyBotResult.items | Where-Object number -eq 402
Assert-True ($policyMentionItem.discussionAssessment.comments[0].actor -eq "author") "An author mentioning the policy bot must remain an author comment."
Assert-True ($policyMentionItem.discussionAssessment.comments[0].author -eq "policy-mention-author") "A policy-bot mention must not replace the comment author's login."
Assert-True ($policyMentionItem.discussionAssessment.comments[0].excerpt -eq "@dotnet-policy-service agree") "The author policy-bot acknowledgement must be assessed unchanged."
Assert-True (-not ($policyBotResult.inbox.community.inventory | Where-Object number -eq 402).responseEvidence.recordedNonAuthorHumanResponse) "An author policy-bot acknowledgement must not count as a non-author human response."
Assert-True (($policyBotResult.inbox.community.inventory | Where-Object number -eq 403).responseEvidence.status -eq "unknown") "Truncated policy-bot-only evidence must remain unknown."

$rankingJson = & $scriptPath `
-InputPath $correctnessFixturePath `
-Now $snapshot `
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
[
{
"number": 401,
"title": "Community contribution with only policy automation",
"url": "https://github.com/dotnet/aspnetcore/pull/401",
"author": { "login": "policy-bot-only-author", "is_bot": false },
"isDraft": false,
"labels": [{ "name": "area-blazor" }, { "name": "community-contribution" }],
"createdAt": "2026-08-25T18:00:00Z",
"updatedAt": "2026-09-01T18:00:00Z",
"headRefOid": "4014014014014014014014014014014014014011",
"headRefName": "policy-bot-only",
"baseRefName": "main",
"mergeable": "MERGEABLE",
"mergeStateStatus": "CLEAN",
"reviewDecision": "REVIEW_REQUIRED",
"statusCheckRollup": [{ "__typename": "CheckRun", "status": "COMPLETED", "conclusion": "SUCCESS" }],
"latestReviews": [],
"reviewRequests": [],
"comments": [{
"author": { "login": "dotnet-policy-service" },
"createdAt": "2026-09-01T18:00:00Z",
"bodyText": "Please rerun validation before merging."
}],
"discussionComments": [{
"author": { "login": "dotnet-policy-service" },
"authorAssociation": "NONE",
"createdAt": "2026-09-01T18:00:00Z",
"bodyText": "Please rerun validation before merging."
}],
"discussionCommentsComplete": true,
"discussionCommentTotalCount": 1,
"discussionThreads": [],
"discussionThreadsComplete": true,
"discussionThreadTotalCount": 0,
"files": [{ "path": "src/Components/Components/src/PolicyBotOnly.cs" }],
"changedFiles": 1
},
{
"number": 402,
"title": "Author acknowledgement mentioning policy automation",
"url": "https://github.com/dotnet/aspnetcore/pull/402",
"author": { "login": "policy-mention-author", "is_bot": false },
"isDraft": false,
"labels": [{ "name": "area-blazor" }, { "name": "community-contribution" }],
"createdAt": "2026-08-25T18:00:00Z",
"updatedAt": "2026-09-02T18:00:00Z",
"headRefOid": "4024024024024024024024024024024024024022",
"headRefName": "policy-mention",
"baseRefName": "main",
"mergeable": "MERGEABLE",
"mergeStateStatus": "CLEAN",
"reviewDecision": "REVIEW_REQUIRED",
"statusCheckRollup": [{ "__typename": "CheckRun", "status": "COMPLETED", "conclusion": "SUCCESS" }],
"latestReviews": [],
"reviewRequests": [],
"comments": [{
"author": { "login": "policy-mention-author", "is_bot": false },
"createdAt": "2026-09-02T18:00:00Z",
"bodyText": "@dotnet-policy-service agree"
}],
"discussionComments": [{
"author": { "login": "policy-mention-author" },
"authorAssociation": "CONTRIBUTOR",
"createdAt": "2026-09-02T18:00:00Z",
"bodyText": "@dotnet-policy-service agree"
}],
"discussionCommentsComplete": true,
"discussionCommentTotalCount": 1,
"discussionThreads": [],
"discussionThreadsComplete": true,
"discussionThreadTotalCount": 0,
"files": [{ "path": "src/Components/Components/src/PolicyMention.cs" }],
"changedFiles": 1
},
{
"number": 403,
"title": "Truncated community discussion showing policy automation",
"url": "https://github.com/dotnet/aspnetcore/pull/403",
"author": { "login": "truncated-policy-author", "is_bot": false },
"isDraft": false,
"labels": [{ "name": "area-blazor" }, { "name": "community-contribution" }],
"createdAt": "2026-08-25T18:00:00Z",
"updatedAt": "2026-09-01T18:00:00Z",
"headRefOid": "4034034034034034034034034034034034034033",
"headRefName": "truncated-policy-bot",
"baseRefName": "main",
"mergeable": "MERGEABLE",
"mergeStateStatus": "CLEAN",
"reviewDecision": "REVIEW_REQUIRED",
"statusCheckRollup": [{ "__typename": "CheckRun", "status": "COMPLETED", "conclusion": "SUCCESS" }],
"latestReviews": [],
"reviewRequests": [],
"comments": [{
"author": { "login": "dotnet-policy-service" },
"createdAt": "2026-09-01T18:00:00Z",
"bodyText": "Please rerun validation before merging."
}],
"discussionComments": [{
"author": { "login": "dotnet-policy-service" },
"authorAssociation": "NONE",
"createdAt": "2026-09-01T18:00:00Z",
"bodyText": "Please rerun validation before merging."
}],
"discussionCommentsComplete": false,
"discussionCommentTotalCount": 51,
"discussionThreads": [],
"discussionThreadsComplete": true,
"discussionThreadTotalCount": 0,
"files": [{ "path": "src/Components/Components/src/TruncatedPolicyBot.cs" }],
"changedFiles": 1
}
]
Loading
Loading