CI Auto-Fix
Diagnose and fix a failed CI check, then verify it passes.
Generic across repositories; currently implements the GitHub Actions path via gh.
This SKILL.md is the orchestration index.
Load the matching rule file when you need detail — do not preload them.
| Phase | Goal | Required rule |
|---|---|---|
| 0 | Resolve the target (run ID / PR URL / auto-detect) | this file |
| 1 | Identify the failure (fetch logs) | this file |
| 2 | Read every workflow file before editing one | this file |
| 3 | Classify the failure with an explicit verdict | rules/verdicts.md + rules/self-improvement-loop.md (read lessons) |
| 3.5 | Write the plan artifact + run the confidence gate | rules/confidence-gate.md + templates/plan-artifact.md |
| 4 | Apply the minimal, targeted fix | this file + rules/anti-patterns.md |
| 5 | Verify locally before pushing | this file |
| 6 | Commit and push (rebase-safe) | this file |
| 7 | Wait for CI and capture the new result | this file |
| 8 | Iterate — with regression detection | rules/regression-detection.md + rules/self-improvement-loop.md (write on revert) |
| 9 | Report (structured exit summary) | this file + rules/self-improvement-loop.md (write on outcome) |
Always read rules/anti-patterns.md first.
The refusals apply to every phase.
Input
The user provides one of:
- A GitHub Actions check/run URL (e.g.
https://github.com/owner/repo/actions/runs/12345678) - A check run ID or workflow run ID
- A PR URL with failing checks (e.g.
https://github.com/owner/repo/pull/42) - Nothing — if
$ARGUMENTSis empty, auto-detect the failing CI for the current branch's PR (see Phase 0).
The argument is: $ARGUMENTS.
Phase 0 — Resolve the target
If $ARGUMENTS is empty, do not ask the user — resolve automatically:
Get the current branch:
git rev-parse --abbrev-ref HEADFind the open PR for this branch:
gh pr list --head "<branch>" --state open --json number,url,headRepositoryOwner --limit 1- If exactly one PR is found, use its URL as the PR input and continue to Phase 1.
- If
headRepositoryOwner.logindiffers from the current repo's owner (fork PR), surface that fact to the user before continuing. - If no open PR is found, fall back to the most recent failed workflow run on this branch:
If a failed run is found, treat itsgh run list --branch "<branch>" --limit 10 --json databaseId,conclusion,workflowName \ | jq '[.[] | select(.conclusion == "failure")] | .[0]'databaseIdas the run ID input. - If neither resolves (no PR, no failed run), then ask the user.
Print the resolved target before continuing:
Auto-detected target: <PR URL or run ID> on branch <branch>.
Step 0: Resolve your GitHub access path
Before any GitHub step, resolve which path you have — gh CLI, mcp__github__* tools, or neither — per agents/shared/rules/github-access.md. Resolve once, state the path you took, and use it for the whole run.
gh is absent in Claude Code cloud sessions, so the commands written below are the gh-path form; on the MCP path use the verb mapping in that file rather than attempting them. With neither path, GitHub steps cannot be performed: say so precisely, do the git work you can, and hand the rest back — never report a step you could not perform as blocked-by-something-else.
Phase 1 — Identify the failure
Based on the input:
Run URL or run ID — fetch the failed job logs:
gh run view <run-id> --log-failedPR URL — list the failing checks first:
gh pr checks <pr-number> --repo <owner/repo>Then fetch logs for each failing check.
Check suite / check run ID:
gh api repos/<owner>/<repo>/check-runs/<check-run-id>
Extract and summarize:
- Which job(s) failed.
- The specific error messages and exit codes.
- Which step within the job failed.
- The full error context (surrounding log lines).
Phase 2 — Understand the workflow holistically
Before making any changes, read every workflow file in the repository:
find .github/workflows -name '*.yml' -o -name '*.yaml'
Build a mental model of:
- How jobs depend on each other (
needs:). - What triggers each workflow (
on:). - Shared steps, reusable workflows, composite actions.
- Environment variables and secrets used.
- Matrix strategies.
- Caching strategies.
- Artifact passing between jobs.
This holistic understanding prevents fixes that solve one problem but break another job or workflow.
Phase 3 — Classify the failure (verdict required)
Pick exactly one verdict per failure. The verdict binds behavior; do not skip this step.
Full decision table and per-verdict notes: rules/verdicts.md.
Verdicts at a glance:
code-bug/workflow-bug/dep-bug/env-bug→ continue to Phase 3.5.flaky/unsure→ escalate. Stop.
Phase 3.5 — Plan artifact + confidence gate
Write or update the plan at
.agent/{branch}/ci-auto-fix-plan.mdusingtemplates/plan-artifact.md. The plan is read-only documentation of intent — the user can pre-empt before any code is written. Once the plan names the failing files, read the sharedcodebase-knowledgesignal for exactly those paths and fold matches into the plan (a known regression hotspot → a tighter fix + a regression check; a recorded invariant → preserve it), perrules/self-improvement-loop.md § Cross-bucket read — codebase-knowledge. This subagent does not runaw-executor, so it is the only point the bucket is read; the read is read-only and never relaxes the confidence gate below.Run the confidence gate per
rules/confidence-gate.md:Score Action ≥ 90 Auto-apply. Continue to Phase 4. 80–89 Show the diff, ask once, apply on approval. < 80 Escalate. Do not write. The gate is non-negotiable.
Phase 4 — Fix the error
Apply the minimal, targeted fix per the verdict:
code-bug— fix the actual code issue.workflow-bug— fix the workflow YAML.dep-bug— update the lockfile or correct the version constraint.env-bug— pin or bump the runner-side version.
Hard refusals (full list in rules/anti-patterns.md):
- Do not disable, skip, or weaken any check.
- Do not add
continue-on-error: true. - Do not add
.skip/it.onlyto silence a test. - Do not skip hooks with
--no-verify. - Do not refactor surrounding code.
Do:
- Make the smallest change that fixes the root cause.
- Stay consistent with the rest of the codebase.
- If fixing a test, verify the test is the one that's wrong (not the code it tests).
Phase 5 — Verify locally
Before pushing, run the same checks that failed:
- If build failed: run the build command.
- If lint failed: run the linter.
- If tests failed: run the tests.
- If typecheck failed: run the type checker.
Only proceed to push if local verification passes.
Phase 6 — Commit and push
Stage only the files relevant to the fix.
Write a clear commit message:
fix(ci): <description of what was fixed> <brief explanation of root cause and fix>Sync with the remote before pushing — a parallel worker may have pushed:
git pull --rebase origin "<branch>"If the rebase conflicts, run
git rebase --abort, stop, and report the conflicting files to the user. Do not auto-resolve.Push:
git push origin "<branch>"If the push is rejected as non-fast-forward, rebase and retry the push once. If the retry also fails, or the rebase conflicts, stop and report. Never
--forcepush from this skill.
Phase 7 — Wait for CI
After pushing, monitor the check:
Find the new workflow run — select on the head SHA, do not sleep and hope. A bare
sleepis blocked in some harnesses, and a fixed 10 s is a race: if registration takes longer, the listing returns the previous commit's runs and you watch a stale run to green. Filtering by SHA removes the race instead of timing it (the same fixe2e-pr-stabilizeralready uses):# Issue this Bash call with the tool parameter timeout: 600000. # Bounded loop with a real interval — registration takes seconds, so six # back-to-back calls would exhaust the retries before it could happen. # The sleep is inside a capped loop, so it is not a bare sleep. timeout 90 bash -c ' SHA=$(git rev-parse HEAD); TMP=$(mktemp) # TERM must be listed: bash runs the EXIT trap on a signal only when that # signal is trapped, and `timeout` sends TERM on the 124 path. trap "rm -f \"$TMP\"" EXIT INT TERM while :; do # stderr -> variable, stdout -> file. `head -1` is deliberately on its own # line below: folding it back into this call would make $? head`s status, # which is 0 even when gh dies. err=$(gh run list --branch <current-branch> --limit 5 \ --json databaseId,headSha,status \ --jq ".[] | select(.headSha == \"$SHA\") | .databaseId" 2>&1 >"$TMP") # gh spoke = gh failed. An empty result with NO stderr is "not registered # yet"; an empty result WITH stderr is a broken gh, and looping on it # would burn the whole budget and then escalate the wrong cause. [ -n "$err" ] && { echo "$err" >&2; exit 3; } NEW_RUN_ID=$(head -1 "$TMP") [ -n "$NEW_RUN_ID" ] && { echo "$NEW_RUN_ID"; exit 0; } sleep 5 done'Exit Outcome Next 0 registeredNEW_RUN_IDis on stdout — watch it3 tooling-failureghitself failed. Report that and escalate — do not retry, and do not report it as "no run found"124 no-run-yetNo run for this SHA after 90 s. Retry the whole block at most twice more, then report and escalate Same classifier as
registration-poll.md— an unrecognisedgherror is never benign, and empty output alone cannot tell "nothing yet" from "nothing works". Different outcome set: this block renders its own 124 asno-run-yetbecause the retry policy lives here, whereas the shared rule keeps its 124 internal. Do not reuse that rule's outcome names.For reference, the unfiltered listing:
gh run list --branch <current-branch> --limit 5Watch the run until completion, bounded per attempt:
# Issue this Bash call with the tool parameter timeout: 600000. # The tool default is 120000; a `timeout` larger than the tool cap never # fires its own exit 124 — the harness kills the call first and the expiry # handling below becomes dead code. timeout 540 gh run watch <new-run-id>If
timeoutexpires (exit code 124), watch again — at most 2 attempts per fix-push cycle, and at most 6 across the whole invocation. Then rungh run view <new-run-id>to capture pending jobs, report them, and escalate.Print each attempt as
ci-watch attempt N/2 (cycle) · M/6 (invocation)and carry those lines into your report. The invocation cap spans multiple Phase 8 iterations — a longer span than any single reasoning step — so it must be written down at the moment it changes, not remembered.State the scope, because two are in play. Each Phase 8 iteration pushes a new commit and therefore watches a new run — a new wait, not a continuation — so a purely per-invocation cap would starve iterations 2–4 of any watch at all. A purely per-cycle cap of 4 would allow 4 × 4 = 16 watches (≈ 2.4 h). The pair above bounds both: per-cycle so each fix gets a fair look, and an invocation ceiling so the total cannot run away.
Your cap is your own. You watch a run for a commit you just pushed, so you never inherit or spend a caller's budget, and you write no shared state. Report your outcome and let the caller act on it.
Check the result:
gh run view <new-run-id>
Phase 8 — Iterate with regression detection
Full decision table: rules/regression-detection.md.
At a glance:
- Same failure → re-classify in Phase 3.
- Strict subset → continue with the remaining failures.
- New failure that did not exist before → revert the last commit (
git revert HEAD && git push) and re-plan or escalate.
Maximum 4 iterations. After 4, escalate with the structured exit summary.
Phase 9 — Report
Always end with a structured summary block, regardless of outcome:
ci-auto-fix run
Outcome: <green | escalated | regression-reverted | max-iterations>
Original failure: <workflow / job / step + one-line cause>
Verdict: <code-bug | workflow-bug | dep-bug | env-bug | flaky | unsure>
Iterations: <N>/4
Plan: .agent/{branch}/ci-auto-fix-plan.md
Successful run: <URL> # if green
Escalation reason: <…> # if not green
On success, include the original error, the fix applied, confirmation that all checks pass, and a link to the successful run.
On escalation, include what was tried (one line per iteration), what remains, and suggested next steps for manual investigation.
Self-Improvement
/ci-auto-fix gets better across runs through a two-tier lessons loop (fast
episodic tier + gated promotion), like autonomous-workflow and fix-bug. It
reads ci-auto-fix-lessons at Phase 3 (biasing the verdict and the Phase 8
regression call) and writes at Phase 8 (on a revert — the strongest negative
signal) and Phase 9 (on the CI outcome). Lessons are advisory — they never
relax the confidence gate, the revert-on-new-failure rule, or any refusal in
rules/anti-patterns.md.
This loop is deliberately more conservative than the others because the
verdict is inferred from CI logs alone: verdict lessons default to the
repo::{owner}/{repo} scope (repo-specific failure shapes are far more
reliable than cross-repo generalizations) with a raised promotion bar
(seen_count >= 5), and regression lessons are volatile with a 30-day
expiry since error signatures churn. A lesson can never authorize a
check-weakening or soft-refusal action — those still re-gate on this run. Full
contract and the two ci-auto-fix-specific entrenchment guards:
rules/self-improvement-loop.md. LoreKit
(the lorekit-memory skill's memory.* tools) is optional; the loop is a silent
no-op if not connected.
Definition of done
The run is done when ANY of the following is true:
- All checks are green AND the structured exit summary has been printed.
- The verdict was
flakyorunsureand the failure was escalated to the user. - The confidence gate scored < 80 and the fix was not written.
- A regression was detected and reverted, and the user owns the next step.
--max-iterations(default 4) was reached.