--- name: fix-pr-checks description: Fix the currently-failing status checks on the current branch's PR in one sweep — route each failing check to the right diagnosis skill (via a Haiku subagent, escalating to Opus on a repeat failure), apply and verify the fix, commit, and push once — then return a summary. Use standalone when CI is red, or as the check-fixing step invoked by /watch-pr. Repo is adamayoung/TMDb. --- # Fix PR Checks Get a pull request's **failing checks** green: for each failing status check, diagnose the cause, apply and verify a fix, and push. This is a **single sweep** over the checks failing *right now* — it diagnoses and fixes them, pushes once, and returns. Waiting on pending checks and deciding whether to sweep again belong to the caller (you, or `/watch-pr`). Repo is `adamayoung/TMDb`. GitHub reads use the **GitHub MCP** (`mcp__github__*`); `gh` is authenticated and used for the blocking CI wait. If an MCP call fails with **401/403** (PAT expired or missing scope), fall back to the equivalent `gh` command. ## It stands on the diagnosis skills Don't read CI logs yourself. The analysis is already a capability — delegate it to a **Haiku subagent** running the matching diagnosis skill (a repeat failure re-diagnoses on **Opus** — see §2), so raw logs never enter your context and you get back a `file:line` cause and a concrete fix: - The **Integration** check (live-API suite from `integration.yml`) → `/diagnose-integration-failure` - Any **CI** check — lint, markdown lint, build, or unit tests from `ci.yml` → `/diagnose-ci-failure` This skill is the **act-on-it** layer: route → fix → verify → commit → push. ## Principles 1. **One sweep, then return.** Handle every check failing at invocation, push once, report. Don't loop waiting for the re-run — that convergence is the caller's job. 2. **Diagnose via Haiku, escalate a repeat to Opus, fix locally.** Get Cause/Fix from the diagnosis skill, then apply it and **verify before claiming it fixed** with the delegated build/test skills. First diagnosis of a check runs on Haiku; a check that comes back after a fix re-diagnoses on Opus (§2). 3. **Respect the attempt cap.** A check gets at most **3** fix→push attempts (tracked in the run ledger). If it still fails on the same root cause, stop touching it and report it exhausted — never loop forever. 4. **Failing only.** Act on `fail` checks. `pending` checks are not yours to chase — report them and leave waiting to the caller. `claude-review` and other neutral checks are non-blocking. 5. **Never fake green.** Don't edit `.github/workflows/*` or CI config to silence a check, and don't force-push, without surfacing to the user first. ## 0. Find the PR Find the open PR for the current branch with `mcp__github__list_pull_requests` (owner/repo from the `origin` remote, `head: :`, `state: open`), or read a specific one with `mcp__github__pull_request_read` method `get`. Take `number`/`html_url`/`state`/`head.ref` from the result. - An explicit PR number in the arguments overrides the current branch. - No PR for the current branch → stop and tell the user (suggest `/pr`). - State not `open` → stop and report. ## 1. List the checks Use `mcp__github__pull_request_read` method `get_check_runs` (owner/repo from `origin`, `pullNumber: `) — the individual CI/CD check runs for the head commit. Classify each by `status` + `conclusion`: - **fix it** (below): `conclusion` is `failure` / `timed_out` / `cancelled` / `action_required`. - **pending** (report, leave for the caller to wait on): `status` is not `completed` (`queued` / `in_progress`). - **ignore**: `conclusion` is `success` / `skipped` / `neutral` (e.g. `claude-review`). If nothing is failing, return immediately (note any pending checks). ## 2. Diagnose each failing check (Haiku first, Opus on a repeat) For each `fail` check under its attempt cap, spawn the **`check-diagnoser`** agent (Agent tool, `subagent_type: check-diagnoser` — its no-fix/no-build contract, report shape, and Haiku floor-pin live in `.claude/agents/check-diagnoser.md`), substituting the check name and the routed skill. Pick the model from the check's ledger history and pass it explicitly (a call-site `model` beats the agent's frontmatter pin — ADR-0014): - **First attempt** → `model: haiku`. - **Repeat** (the ledger shows a prior attempt for this check — a fix that didn't verify, or the check failed again after the push) → `model: opus`, prepending one line of prior-attempt context to the prompt: the previous Cause/Fix and why it didn't stick. A misdiagnosis costs a full fix→push→CI round trip; don't pay it twice on the same model tier. > **Registry lag.** A freshly merged agent file may not register as a > spawnable `subagent_type` until a new session > (`knowledge/delivery-retros.md`), and an unrecognised type silently starts a > plain general-purpose agent — with no contract at all now that the prompt > below carries none. If `check-diagnoser` is not a recognised type, don't > fall through silently: say so, and re-spawn as `general-purpose` with > `.claude/agents/check-diagnoser.md` read into the prompt as its contract. ```text The `` check failed on the TMDb PR for branch ``. Use the `` skill to diagnose it. ``` ### Two or more failing checks → one Workflow One failing check stays a direct Agent call; a Workflow for a single agent is pure overhead. At **two or more**, fan out instead — one diagnosis agent per check, each schema-validated, with `model` set per agent from the ledger history so the Haiku→Opus escalation survives (a Workflow cannot see that history on its own — pass it in via `args`). Run the script below via the `Workflow` tool with `args: { branch, checks: [{ name, skill, priorAttempt }] }` — `priorAttempt` is the ledger's record for that check (omit on a first attempt). It is embedded here, not a file, because it runs **once per invocation** — the same rule that keeps `/review-plan`'s and `/review-changes`'s scripts embedded, while `deliver-panel.js` lives in `.claude/workflows/` (it runs at every panel decision point — seven of them — in a single run). Invoking this skill is itself the opt-in to call `Workflow`. The guards are **executable `throw`s, not instructions**: a malformed payload must fail at agent zero, never mid-fan-out. ```javascript export const meta = { name: 'fix-pr-checks-diagnose', description: 'Diagnose each failing PR check in parallel, one read-only agent per check', phases: [{ title: 'Diagnose', detail: 'one agent per failing check' }], } // `args` can arrive as a JSON STRING rather than an object (a known harness // gotcha). Parsing is not optional: iterating a string character-by-character // once fanned out ~281 agents into a spend limit, so validate the SHAPE before // spawning anything. const input = typeof args === 'string' ? JSON.parse(args) : args if (!input || typeof input !== 'object') { throw new Error('fix-pr-checks: args must be an object (or a JSON string encoding one).') } if (!Array.isArray(input.checks)) { throw new Error( `fix-pr-checks: args.checks must be an ARRAY, got ${typeof input.checks}. ` + `Refusing to fan out — iterating a non-array here is the ~281-agent failure.` ) } if (!input.checks.length) throw new Error('fix-pr-checks: args.checks is empty — nothing to diagnose.') // Element shape too, not just the container. `checks: ['CI', 'Integration']` is the // natural malformed form: it passes an Array.isArray check, then spawns agents whose // prompts read "the `undefined` check failed" and reports full coverage, because the // dead-detection compares `undefined === undefined`. A `null` element instead throws // at the dead-detection — AFTER the agents ran, discarding their work. Both violate // "fail at agent zero", so validate here. if (!input.checks.every((c) => c && typeof c === 'object' && typeof c.name === 'string' && c.name && typeof c.skill === 'string' && c.skill)) { throw new Error( 'fix-pr-checks: every entry in args.checks must be an object { name, skill } with ' + 'non-empty strings. Refusing to fan out on a malformed element.' ) } if (typeof input.branch !== 'string' || !input.branch) { throw new Error('fix-pr-checks: args.branch must be a non-empty string.') } const DIAGNOSIS_SCHEMA = { type: 'object', additionalProperties: false, properties: { check: { type: 'string' }, summary: { type: 'string', description: 'one or two sentences on what failed' }, likelyCause: { type: 'string', description: 'the ranked root cause' }, suggestedFix: { type: 'string', description: 'the concrete next step' }, fileLine: { type: 'string', description: 'the offending file:line, or "unknown"' }, observed: { type: 'string', description: 'for an Integration check: the live call made and the shape returned, or "unavailable (headless)"; ' + 'omit for CI checks. NEVER include a URL, command or header carrying TMDB_API_KEY.', }, }, required: ['check', 'summary', 'likelyCause', 'suggestedFix'], } phase('Diagnose') const results = await parallel(input.checks.map((c) => () => agent( `The \`${c.name}\` check failed on the TMDb PR for branch \`${input.branch}\`.\n\n` + `Use the \`${c.skill}\` skill to diagnose it.` + (c.priorAttempt ? `\n\nTHIS IS A REPEAT. The previous attempt did not stick:\n${c.priorAttempt}\n` + `Do not re-propose it — find what that diagnosis missed.` : ''), { label: `diagnose:${c.name}`, phase: 'Diagnose', // The agent definition carries the no-build rule and report contract. agentType: 'check-diagnoser', // Haiku first; a repeat escalates. The conductor supplies the history — // a Workflow cannot see the ledger's per-check attempt counters. model: c.priorAttempt ? 'opus' : 'haiku', schema: DIAGNOSIS_SCHEMA, } ).then((r) => r && { ...r, check: c.name }) )) const live = results.filter(Boolean) const dead = input.checks.filter((c) => !live.some((r) => r.check === c.name)).map((c) => c.name) if (dead.length) log(`WARNING: no diagnosis returned for ${dead.join(', ')} — treat as undiagnosed, not as clean`) return { diagnoses: live, undiagnosed: dead } ``` **The fix stays here.** Only diagnosis is delegated; §3's apply → verify → commit and §4's single batched push run in the conductor, which owns the build slot and the attempt counters. A check in `undiagnosed` is **not** clean — leave it and report it. ## 3. Apply, verify, commit From the returned Cause/Fix: 1. **Apply the fix** to the offending `file:line`. 2. **Verify locally** with the matching delegated skill — `/lint`, `/build`, `/test`, or `/integration-test` (they run in Haiku subagents; logs stay out of context). 3. **Commit** with a gitmoji message; record the SHA. **Batch the sweep's pushes** — commit per fix, but `git push` **once** after all failing checks are handled (§4), so the re-run covers everything in one CI cycle. 4. Increment that check's attempt counter in the ledger. If the diagnosis is ambiguous or the fix is risky, don't guess — leave the check, note it for the user, and move on. ## 4. Push once, then finish After all failing checks are handled, if you committed any fixes, `git push` once (it re-triggers CI — expected). **There is no pre-commit hook** (`.git/hooks/` holds only `*.sample`), so nothing lints on your behalf — run `/lint` before committing, or a lint fix can itself land a lint failure. ## Return: sweep summary Report, concisely: - **Fixed** — check name → cause (`file:line`) → commit SHA. - **Exhausted** — checks at the 3-attempt cap still failing on the same root cause; stop and hand to the user. - **Skipped/ambiguous** — failing checks you intentionally left (risky/unclear), with why. - **Pending** — checks still in progress, left for the caller to wait on. - Whether you pushed (and thus re-triggered CI) so the caller can decide whether to sweep again. Arguments: $ARGUMENTS