Fix PR Checks

SkillAI & models

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Fix PR Checks skill

What this skill tells your AI

The instructions your AI receives, as published by adamayoung/tmdb in .claude/skills/fix-pr-checks/SKILL.md and read by ahel’s review.

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: <owner>:<branch>, 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: <n>) — 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 attemptmodel: 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.

The `<CHECK NAME>` check failed on the TMDb PR for branch `<branch>`.

Use the `<SKILL>` 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 throws, not instructions: a malformed payload must fail at agent zero, never mid-fan-out.

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

Signals

GitHub stars
176
Forks
47
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
fix-pr-checks
Source
github.com/adamayoung/tmdb