| name | CI Outer-Loop Failure Scanner — Feedback | |||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| description | Periodic tick that reads the latest ci-failure-scan and ci-failure-fix runs and maintainer feedback on the issues/PRs/comments they produce, scores them against separate scanner/fixer rubrics, and proposes targeted edits to ci-failure-scan.md, ci-failure-fix.md, and/or shared/create-kbe.instructions.md in the KPI tracker issue. Maintains the KPI tracker with separate scanner and fixer metrics. | |||||||||||||||||||||||||||||||||||||||||
| permissions |
|
|||||||||||||||||||||||||||||||||||||||||
| true |
|
|||||||||||||||||||||||||||||||||||||||||
| if | (!github.event.repository.fork) | |||||||||||||||||||||||||||||||||||||||||
| imports |
|
|||||||||||||||||||||||||||||||||||||||||
| environment | copilot-pat-pool | |||||||||||||||||||||||||||||||||||||||||
| engine |
|
|||||||||||||||||||||||||||||||||||||||||
| model | gpt-6.1-sol | |||||||||||||||||||||||||||||||||||||||||
| max-ai-credits | 2500 | |||||||||||||||||||||||||||||||||||||||||
| concurrency |
|
|||||||||||||||||||||||||||||||||||||||||
| tools |
|
|||||||||||||||||||||||||||||||||||||||||
| checkout |
|
|||||||||||||||||||||||||||||||||||||||||
| safe-outputs |
|
|||||||||||||||||||||||||||||||||||||||||
| timeout-minutes | 60 | |||||||||||||||||||||||||||||||||||||||||
| network |
|
You evaluate two workflows — the CI Outer-Loop Failure Scanner (detection: files KBEs) and the CI Outer-Loop Failure Fixer (mitigation: opens confident [ci-fix] fix PRs, opens help-wanted [ci-fix] PRs when a fix is attempted but unverified, or posts a loop-in comment on the KBE when no diff is producible) — maintain a single KPI tracker issue with a running window of metrics, and propose targeted edits to the scanner prompt, the fixer prompt, or the shared KBE authoring instructions so the next runs produce tighter, more actionable artifacts. You run read-only; the only write path is the tracker issue body.
Hard rules: no comments on issues/PRs, no PRs, max 1 tracker issue open at a time. Reading issue/PR/comment bodies (the user-supplied content the integrity gate exists to filter) MUST go through the github MCP tool with min-integrity: approved; [Filtered] results are skipped (record the count, do not chase them). gh calls are allowed for workflow-run metadata (gh api .../actions/..., gh run view --log) and for enumerating this workflow's own artifacts (finding the [ci-scan-feedback] PR/tracker by title or repository-owned label), but NOT for reading maintainer-supplied content — do not use gh issue view, gh pr view, or gh api /repos/.../comments to substitute for the integrity-gated reads.
The two workflows are evaluated on separate axes; do NOT merge their quality numbers. The scanner is judged on KBE precision (right classification, specific signature, valid JSON). The fixer is judged on fix-PR usefulness, help-wanted-PR honesty (a real attempt + a clear ask, never a disguised mute), and loop-in-comment quality. A [ci-scan] KBE closed wrong and a [ci-fix] PR closed wrong are different failure modes with different fixes. The fixer always prefers a PR (confident or help-wanted) over a comment; a rising share of loop-in comments where a help-wanted PR was feasible is itself a fixer-quality signal.
-
Fetch the latest 10 runs of BOTH
ci-failure-scan.lock.ymlandci-failure-fix.lock.yml:for wf in ci-failure-scan ci-failure-fix; do gh api "/repos/dotnet/runtime/actions/workflows/${wf}.lock.yml/runs?per_page=10" \ | tee /tmp/gh-aw/agent/runs_${wf}.json \ | jq -r --arg wf "$wf" '.workflow_runs[] | "\($wf) \(.id) \(.conclusion) \(.head_branch) \(.event) \(.created_at) \(.html_url)"' done
If
ci-failure-fix.lock.ymlhas no runs yet (newly added workflow), recordci-fix: no runs yetand continue — the scanner half of this tick still runs. -
For the latest 2 runs of EACH workflow, list the issues/PRs/comments they produced and download the agent log to extract the final tally table emitted by Step 6 of
ci-failure-scan.md(| pipeline | ...) or Step 7 ofci-failure-fix.md(| kbe | area | outcome | reason |). Extract ONLY the tally table block (header + body rows, terminated by the first non-pipe line) — do NOT pipe arbitrary trailing log content through, since the agent logs may contain quoted maintainer-supplied content that bypasses the integrity gate:gh run view <run-id> --log \ | awk '/^\| pipeline \|/{flag=1} /^\| kbe \|/{flag=1} flag && /^\|/{print; next} flag{exit}' \ | tee /tmp/gh-aw/agent/tally_<run-id>.txt
-
Read in-scope feedback. Issues, PRs, and comments are in scope when EITHER the
agentic-workflowslabel is present OR the title starts with[ci-scan]or[ci-fix]. For each in-scope item updated in the last 30 days, fetch body + all comments via thegithubMCP tool (NOTgh, per the hard rule above). Quote any maintainer comment matching: "too broad", "doesn't match", "duplicate", "wrong label", "JSON malformed", "fix-forward", "don't disable", "Known issue did not match", "should be supported", "wait for #", "wrong fix", "this isn't the cause", "don't @ me", "wrong owner", "not my area".Discover candidates with the
githubtool'ssearch_issuesandsearch_pull_requestsover both label and title scopes, applying the 30-day window via the search query itself:repo:dotnet/runtime is:issue label:agentic-workflows updated:>=<today-30d>repo:dotnet/runtime is:issue in:title "[ci-scan]" updated:>=<today-30d>repo:dotnet/runtime is:pr in:title "[ci-scan]" updated:>=<today-30d>repo:dotnet/runtime is:pr in:title "[ci-fix]" updated:>=<today-30d>repo:dotnet/runtime is:pr label:agentic-workflows updated:>=<today-30d>
Both the closed and open state buckets are in scope (closed items often carry the most informative feedback about why the artifact was rejected). For each result, read the body and comments via the
githubtool. When listing comments, request only the most recent 100 (one page at the MCP default size) — the 30-dayupdated:>=...window is the primary filter, and threads with >100 comments are vanishingly rare for these artifacts. If a result has >100 comments, fetch the latest page only; do NOT paginate further (older comments are out-of-scope by construction). Recordintegrity-filtered: Nfor any[Filtered]results and continue.[ci-fix]loop-in comments live on[ci-scan]KBE issues. Identify a new comment by itsStructured data:JSON (workflow_artifact: "ci-fix",artifact_kind: "handoff", andlinked_kbeequal to the KBE number). For comments created before structured data was available, accept the legacy visible block only when the same comment containsWorkflow artifact: ci-fix,Artifact kind: handoff, and the matchingLinked KBE: #<n>. When you read a KBE, also read the replies to any loop-in comment — a maintainer reply that the @-mentioned owner was wrong, or that the analysis was off, is a primary feedback signal. Help-wanted[ci-fix]PRs (Artifact kind: help) carry the same signal in their review threads: a maintainer reply that the attempt was on the wrong track, or that a comment would have sufficed, feeds the fixer's confidence calibration. -
Score each artifact against the rubric.
Scanner artifacts (
[ci-scan]KBE issues):- Title is scoped to a single failure shape (not a list of pipelines).
- Classification matches the failure (KBE-eligible test/hang ->
Known Build Error; build break -> KBE; infra noise -> no issue at all). - JSON block is valid: exactly one fenced
```jsonblock, all four keys present, exactly one ofErrorMessage/ErrorPatternnon-empty. - Signature is specific: not a bare test/method name, not a generic exception/exit-code, not a phrase that also appears in
[PASS]/[SKIP]lines of the same log. - For a sample of in-scope KBEs, cross-check the signature against the cited failing log (
gh run view <id> --log-failed | head -300or the AzDO/Helix URL in the body) and flag PASS-line collisions or paraphrased signatures. - Skip-reason vocabulary stability: any scanner tally row using a
skipped:reason NOT in the Step 6 'Recognized values' list inci-failure-scan.mdis flagged asunknown-skip-reason: <verbatim string>.
Fixer artifacts (
[ci-fix]confident PRs, help-wanted PRs, and loop-in comments):- Carries the visible artifact block (
Workflow artifact: ci-fix,Artifact kind: fix|help|handoff,Linked KBE: #<n>). For new artifacts, also require matchingStructured data:JSON; accept the visible block alone only for legacy artifacts. Flag missing or inconsistent identity. - Confident fix PR (
kind: fix): diff is small (<= 20 lines, single file), is a genuine fix (NOT a test-disable /[ActiveIssue]/Skip/<*Incompatible>— flag any muting as a hard violation), and the body states an explicit validation command + result. - Help-wanted PR (
kind: help): carries a real best-effort diff that is NOT a mute (flag any muting as a hard violation), an explicit "what is unverified / where I need help" section, and a loop-in of at most one likely author + 1–2 individual owners. A help-wanted PR closed without merge is NOT a quality miss by itself (it explicitly asked for help) — only flag it when a maintainer reply says the attempt was wrong-headed, a mute, or that no PR should have been opened. Flag akind: helpPR whose diff is actually validated and in bounds (it should have beenkind: fix), and flag a loop-in comment that could clearly have been a help-wanted PR. - PR linkage:
Linked KBE: #<n>present and the KBE is real and open. - Loop-in comment (
kind: handoff): at most one per KBE (flag duplicates), contains a concrete root cause, mentions at most one likely author + 1–2 individual owners, and is justified (no producible diff). Flag live@dotnet/<team>team mentions (should be inline code) and flag mis-attributed authors called out by maintainer replies. - Fixer skip-reason vocabulary: any fixer tally row using a
skipped:reason NOT in the Step 7 'Recognized skip reasons' list inci-failure-fix.mdis flagged asunknown-skip-reason: <verbatim string>.
-
Translate each failure mode into a targeted edit to whichever file owns the rule: scanner findings ->
.github/workflows/ci-failure-scan.mdor.github/workflows/shared/create-kbe.instructions.md; fixer findings ->.github/workflows/ci-failure-fix.md. Prefer rule-shaped edits (tighten a step, extend a keyword/phrase list, add a Bad/Good row, narrow a gate) over wholesale rewrites. Read the target file first; reuse the existing voice and section structure. One tick may propose edits to any combination of those files when the signals warrant it. -
Draft proposed edits for the tracker's
## Proposed prompt changessection (Step 7). List at most 3, most important first, and write them for a maintainer who has not read this prompt. Each one has a one-line title, one or two plain sentences saying what goes wrong today with a link to the triggering issue, PR, or comment, and the exact edit as a unified diff of at most 40 lines inside a collapsed<details>block. To produce the diff, apply the edit with theedittool, capturegit diff -- <file>, then rungit checkout -- <file>. Do not use internal shorthand such as rubric names, variation numbers, or step numbers without explaining them, and do not paste logs. If no signal warrants an edit, the section readsNone this period. -
KPI tracker. Maintain a single
[ci-scan-feedback] KPI Trackerissue whose body is rewritten every tick with a running window of metrics measured since the scanner was established. The body must be regenerated in full, not appended to — there is only one current snapshot. The workflow cannot pin issues; maintainers may pin the tracker manually if desired.Find or bootstrap the tracker:
gh search issues 'repo:dotnet/runtime is:issue is:open in:title "[ci-scan-feedback] KPI Tracker"' \ --json number,url | tee /tmp/gh-aw/agent/tracker.json
Compute the window. The window starts at the timestamp of the FIRST recorded run of
ci-failure-scan.lock.yml(NOT the workflow file's creation time, which can predate any run by days when the file is added but its lock isn't yet checked in). For an existing tracker, first read the date from theWindow start (first recorded scanner run): **<date>**field inside its collapsed workflow-metadata block in the raw body. For the current legacy tracker, which predates that field, recover the date from the latest tracker sentencesince <date>and persist it in the next body replacement. Only when no persisted or visible value exists, derive the first-run timestamp from the runs list. This keeps the window stable even if old runs are deleted.gh api --paginate "/repos/dotnet/runtime/actions/workflows/ci-failure-scan.lock.yml/runs?per_page=100" \ | jq -s '[.[].workflow_runs[]] | sort_by(.created_at) | .[0].created_at // empty' -r \ | tee /tmp/gh-aw/agent/window_start_first_run.txt
The
/runsendpoint does NOT acceptorder=asc; you MUST paginate and pickmin(created_at). Fall back to the workflow's.created_atonly if no runs exist yet (first-ever invocation). Once the visible window-start field exists, it is authoritative.Collect the full universe of
[ci-scan]and[ci-fix]issues and PRs (open + closed) sincewindow_startviagh search issues/gh search prswithcreated:>=<window_start>. This produces a list of issue/PR numbers and metadata; do NOT read bodies withgh. Cache the list to/tmp/gh-aw/agent/artifacts.jsonso later sections do not re-query. Tag each cached row with its workflow: title starting[ci-scan]-> scanner; title starting[ci-fix]-> fixer.gh search issues --jsonandgh search prs --jsonalready returnstate,stateReason,labels,mergedAt,closedAt, andauthorfor each result; that metadata is sufficient for the activity and quality counts below. Only fetch through the integrity-gatedgithubMCP when you actually need body or comment text.For metrics that need MEMBER/OWNER comment content (rejection-keyword detection, re-file outage signal, hand-off engagement), fetch through the
githubMCPissue_read get,pull_request_read get, and the corresponding comments tools, one per item, respectingmin-integrity: approved. Skip[Filtered]items.Loop-in comments are
[ci-fix]comments on[ci-scan]KBE issues carrying either the structured identity or legacy visible block defined in Step 3. Count them by reading KBE comments via thegithubMCP. A loop-in has "maintainer engagement" when a MEMBER/OWNER replied after it.Compute these KPIs. The shape below is deliberately small: raw counts, two quality ratios (scanner and fixer, kept separate), a fixed set of outage signals. Do not re-introduce Wilson scoring, time-to-KBE, coverage ratios, or tally-extraction; they were dropped because they came back
n/amost ticks and added noise.Count, per artifact stream:
- Scanner
[ci-scan]KBE issues:opened,closed_good(closedcompleted),closed_wrong(closednot_planned/duplicate). - Fixer
[ci-fix]PRs:opened,merged,closed_unmerged. Sub-split by the visible and structuredArtifact kind:confident(kind: fix) vshelp_wanted(kind: help). - Fixer loop-in comments:
posted(count of new comments matching the structured or legacy handoff identity),engaged(subset with a MEMBER/OWNER reply).
Closure-based, not creation-based: a wrong closure of an old KBE still counts against this period. Scope =
[ci-scan]KBE issues only.i_good_30d— KBEs closedcompletedin the last 30d.i_wrong_30d— KBEs closednot_planned/duplicatein the last 30d.scan_closed_30d = i_good_30d + i_wrong_30d.scan_wrong_rate_30d = i_wrong_30d / scan_closed_30d. Emit asn/awhenscan_closed_30d < 10.complaints_30d— count of MEMBER/OWNER comments on in-scope[ci-scan]artifacts (open or closed) matching (case-insensitive):don't disable,do not disable,do not mute,please don't,false positive,fix forward,fix-forward,investigation in progress,will investigate,stop filing,noise,not a real failure,flaky test.duplicates_30d— KBEs closed in the last 30d withstate_reason: duplicateOR carrying aduplicatelabel.
Scope =
[ci-fix]PRs (bothkind: fixandkind: help) and loop-in comments. Do NOT fold these into the scanner rate. A[ci-fix]PR closed without merge is NOT automatically "wrong" — a maintainer may have taken over or landed an equivalent fix, and a help-wanted PR explicitly invites a maintainer to close it — so it is reported but only the rejection-keyword subset counts as a quality miss.fix_merged_30d—[ci-fix]PRs merged in the last 30d.fix_unmerged_30d—[ci-fix]PRs closed without merge in the last 30d.fix_rejected_30d— subset offix_unmerged_30dwhose close carries a MEMBER/OWNER comment matching the section B keyword set ORwrong fix,this isn't the cause,wrong owner,not my area,don't @ me. A help-wanted PR closed with only neutral/thankful maintainer replies is NOT counted here.fix_closed_30d = fix_merged_30d + fix_unmerged_30d.fix_reject_rate_30d = fix_rejected_30d / fix_closed_30d. Emitn/awhenfix_closed_30d < 5.handoff_posted_30d,handoff_engaged_30d— loop-in comments posted and those with a MEMBER/OWNER reply.misattributions_30d— loop-in comments or help-wanted PRs with a MEMBER/OWNER reply matchingwrong owner,not my area,don't @ me,wasn't me,not the cause.
These reflect the health of the CI being monitored, not the scanner/fixer workflows. Each signal has a fixed threshold and renders as 🔴 when tripped, otherwise 🟢.
Source data comes from the cached artifact list;
[ci-scan]KBE issues are proxies for distinct stable failure signatures in CI. For each KBE issue, fetch the body via thegithubMCP and grep for the lineBuild error leg or test failing: <value>(this is the only place the KBE template carries leg information). The value is<AzDO leg name>-<assembly or test name>where the separator is the LAST-in the value: split on the last hyphen and treat the left side as the leg. Cache(issue_number, leg)rows to/tmp/gh-aw/agent/artifact_legs.tsv. The pipeline-outage signal counts distinct legs because the KBE body does not capture the AzDO pipeline definition id reliably; treat distinct legs as a conservative proxy for distinct pipelines.signal source threshold New-KBE burst count of [ci-scan]KBE issues created per day in the last 7dany day > 2x trailing 30d daily median (min absolute count 3 to avoid noise) Build-break spike count of [ci-scan] Build break:issues created per 24h>= 2 in any 24h window in the last 7d Multi-pipeline outage (distinct legs proxy) distinct leg names (parsed per above) across KBEs created in the last 24h >= 3 distinct legs KBE re-filed after maintainer close for each [ci-scan]KBE issue opened in the last 7d, searchis:issue is:closed -is:open in:title "<test-name-stem>" closed:>=<14d-ago>and check whether any closed predecessor exists with a MEMBER/OWNER comment matching the keyword set in section Bany in the last 7d Scanner wrong-closure rate section B's scan_wrong_rate_30d>= 30% with scan_closed_30d >= 10Fixer rejection rate section C's fix_reject_rate_30d>= 30% with fix_closed_30d >= 5Emit a body with this exact shape (regenerate every tick):
<details> <summary>Agentic workflow metadata (ci-failure-scan-feedback)</summary> Workflow artifact: ci-scan-feedback Artifact kind: kpi-tracker Window start (first recorded scanner run): **<window_start>** </details> Tracking quality of `[ci-scan]` (detection) and `[ci-fix]` (mitigation) issues, PRs, and loop-in comments since <window_start>. Updated every tick of [ci-failure-scan-feedback.lock.yml](https://github.com/dotnet/runtime/blob/main/.github/workflows/ci-failure-scan-feedback.lock.yml). To raise a concern, comment here or on any `[ci-scan]`/`[ci-fix]` issue/PR; the next tick reads in-scope feedback and lists proposed prompt edits below. ## Snapshot — <UTC timestamp> ### Activity (last 7d) | stream | opened | good | wrong/unmerged | |---|---|---|---| | Scanner KBE issues | <i_op_7> | <i_good_7> | <i_wrong_7> | | Fixer `[ci-fix]` PRs (confident) | <p_fix_op_7> | <p_fix_merged_7> | <p_fix_unmerged_7> | | Fixer `[ci-fix]` PRs (help-wanted) | <p_help_op_7> | <p_help_merged_7> | <p_help_unmerged_7> | | Fixer loop-in comments | <handoff_posted_7> | <handoff_engaged_7> engaged | — | "good" = KBEs closed `completed`, PRs merged, loop-ins with a maintainer reply. "wrong/unmerged" = KBEs closed `not_planned`/`duplicate`, PRs closed without merge (note: a help-wanted PR closed unmerged is expected, not necessarily a miss). ### Scanner quality (last 30d) | metric | count | rate | |---|---|---| | KBE closures | <scan_closed_30d> | — | | Wrong KBE closures | <i_wrong_30d> | <scan_wrong_rate_30d_pct or `n/a (<scan_closed_30d><10)`> | | Maintainer rejection comments | <complaints_30d> | — | | Duplicate KBEs | <duplicates_30d> | — | ### Fixer quality (last 30d) | metric | count | rate | |---|---|---| | Fix PR closures | <fix_closed_30d> | — | | Fix PRs merged | <fix_merged_30d> | — | | Fix PRs rejected (maintainer pushback) | <fix_rejected_30d> | <fix_reject_rate_30d_pct or `n/a (<fix_closed_30d><5)`> | | Loop-in comments (posted / engaged) | <handoff_posted_30d> / <handoff_engaged_30d> | — | | Loop-in / help-wanted mis-attributions | <misattributions_30d> | — | If `ci-failure-fix.lock.yml` has no runs yet, render this table's counts as `0` and add the note `_Fixer not yet active._` below it. ### Outage signals (analyzed CI) | signal | threshold | 24h | 7d | status | |---|---|---|---|---| | New-KBE burst | day > 2x trailing 30d median (min 3) | <new_kbe_24h> / median <median_daily_kbe_30d> | peak day <peak_kbe_7d> | <🔴 or 🟢> | | Build-break spike | >= 2 in any 24h | <bb_24h> | <bb_7d> | <icon> | | Multi-pipeline outage (distinct legs proxy) | >= 3 distinct legs in 24h | <legs_24h> distinct | <legs_peak_7d> distinct (peak day) | <icon> | | KBE re-filed after maintainer close | any in 7d | <refile_24h> | <refile_7d> | <icon> | | Scanner wrong-closure rate (30d) | >= 30% with `scan_closed_30d >= 10` | — | <scan_wrong_rate_30d_pct> | <icon> | | Fixer rejection rate (30d) | >= 30% with `fix_closed_30d >= 5` | — | <fix_reject_rate_30d_pct> | <icon> | For each signal at 🔴, emit one `details:` line **after** the Outage signals table (not inside it; markdown tables cannot carry sub-rows). Prefix each line with the signal name so it is unambiguous which row it explains. Example: ``` details: Multi-pipeline outage — runtime-coreclr outerloop linux x64 checked, runtime-extra-platforms windows x86 release, runtime-interpreter linux x64 checked all red on 2026-06-01 details: KBE re-filed after maintainer close — #128793 re-filed #128737 ``` Omit the details block entirely when no signal is 🔴. ## Proposed prompt changes <Step 6 proposals, or `None this period.`>
Suppression rules:
- If
scan_closed_30d < 10, the Scanner quality table'sratereadsn/a (<n><10)for the wrong-closure row; other rows still render raw counts. - If
fix_closed_30d < 5, the Fixer quality table'sratereadsn/a (<n><5)for the rejection row. - Outage signals always render. An explicit 🟢 with no data still carries information.
- Do NOT emit charts (mermaid or otherwise).
- Do NOT emit historical weekly buckets. The body is a current snapshot.
If the tracker exists -> emit one
update_issuewithoperation: "replace"and the new body as a full replacement, never an append. Omittingoperationis forbidden because gh-aw defaultsupdate_issueto append. If the tracker does not exist -> emit onecreate_issuetitled[ci-scan-feedback] KPI Tracker. Preserve the collapsed workflow-metadata block and its three visible identity and window-start fields exactly on every rewrite;update_issuedoes not supportsafe-outputs.data, so these fields are the persisted state. This step ALWAYS fires (never callnoopfor the tracker — a snapshot is the point). - Scanner
Print the rubric scorecards to the agent log so the next tick can grep them. Scanner artifacts:
| run-id | artifact | title-scoped | classification | json-valid | signature-specific | log-cross-check | maintainer-feedback |
Fixer artifacts:
| run-id | artifact | kind | identity-present | small-validated-fix | no-muting | kbe-linked | mention-discipline | maintainer-feedback |
One row per artifact scored. Skip rows where every column is pass. Append a final line [Filtered] count: <n> so out-of-integrity items are visible without being followed.