Skip to content
Development
Command

/fix-ci

Automated CI failure resolution loop — diagnoses failures on a PR, applies targeted fixes, pushes, and loops until green or max attempts reached.

BOOST
From plugin
forgedock
11980 skills80 commands
Install
> /plugin marketplace add RapierCraftStudios/ForgeDock
> /plugin install forgedock@forgedock

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/fix-ci

Context preview

What this command does when you run it.

Automated CI failure resolution loop — diagnoses failures on a PR, applies targeted fixes, pushes, and loops until green or max attempts reached.

Command definition

fix-ci.md
description: Automated CI failure resolution loop — diagnoses failures on a PR, applies targeted fixes, pushes, and loops until green or max attempts reached.
argument-hint: <PR number or URL> [--max-attempts N] [--repo owner/repo]

<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios --> <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->

/fix-ci — Automated CI Failure Resolution

**Input**: $ARGUMENTS

**Invoked by**: `/deploy-pr` (CI gate step) or directly by a user when a PR's CI is failing.

**Output**: Structured result — `{ pr, status, attempts, fixes_applied }` — printed to stdout and available to the calling skill.

**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. **NEVER use plan mode (EnterPlanMode).** **NEVER force-push or amend commits** — always create new commits.

<!-- FORGE:SPEC_LOADED — fix-ci.md loaded and active. Agent is bound by this spec. -->

---

Config Preamble

Read `forge.yaml` before running any phase:

CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
if [ ! -f "$CONFIG_FILE" ]; then
  echo "ERROR: forge.yaml not found. Run: npx forgedock init"
  exit 1
fi
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
GH_FLAG="-R ${GH_REPO}"
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
STAGING_BRANCH=$(yq '.branches.staging // "staging"' "$CONFIG_FILE")

Argument Parsing

Parse `$ARGUMENTS`:

| Argument | Variable | Default | |----------|----------|---------| | PR number or URL | `PR_NUMBER` | (required) | | `--max-attempts N` | `MAX_ATTEMPTS` | `5` | | `--repo owner/repo` | `GH_REPO` | from forge.yaml |

# Extract PR number (accept raw number or full GitHub URL)
PR_INPUT=$(echo "$ARGUMENTS" | awk '{print $(1)}')
if echo "$PR_INPUT" | grep -qE '^https?://'; then
  PR_NUMBER=$(echo "$PR_INPUT" | grep -oE '[0-9]+$')
else
  PR_NUMBER=$(echo "$PR_INPUT" | tr -d '#')
fi

MAX_ATTEMPTS=5
if echo "$ARGUMENTS" | grep -q -- "--max-attempts"; then
  MAX_ATTEMPTS=$(echo "$ARGUMENTS" | grep -oE -- '--max-attempts [0-9]+' | awk '{print $(2)}')
fi

if [ -z "$PR_NUMBER" ]; then
  echo "ERROR: PR number or URL is required."
  echo "Usage: /fix-ci <PR number or URL> [--max-attempts N]"
  exit 1
fi

echo "fix-ci: PR #${PR_NUMBER}, max attempts: ${MAX_ATTEMPTS}"

---

Phase 0: Assess the PR

0A: Load PR state

PR_DATA=$(gh pr view "$PR_NUMBER" $GH_FLAG \
  --json number,title,headRefName,baseRefName,state,isDraft,url,headRepository 2>/dev/null)

if [ -z "$PR_DATA" ]; then
  echo "ERROR: PR #${PR_NUMBER} not found or not accessible."
  exit 1
fi

PR_HEAD=$(echo "$PR_DATA" | jq -r '.headRefName')
PR_BASE=$(echo "$PR_DATA" | jq -r '.baseRefName')
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
PR_URL=$(echo "$PR_DATA" | jq -r '.url')

echo "PR #${PR_NUMBER}: ${PR_HEAD} → ${PR_BASE} (${PR_STATE})"

if [ "$PR_STATE" = "MERGED" ]; then
  echo "PR #${PR_NUMBER} is already merged — nothing to fix."
  echo '{"pr": '"$PR_NUMBER"', "status": "already_merged", "attempts": 0, "fixes_applied": []}'
  exit 0
fi

if [ "$PR_STATE" = "CLOSED" ]; then
  echo "PR #${PR_NUMBER} is closed — cannot fix CI on a closed PR."
  exit 1
fi

0B: Determine fix target branch

The fix target is the branch the PR's **head** points to. CI fixes are committed directly to the PR's head branch — not to the base branch.

# Smart target detection: where do fixes land?
# - staging → main PRs:   fixes go to staging (the head branch)
# - milestone/* → staging: fixes go to the milestone branch (the head branch)
# - feature/* PRs:         fixes go to the feature branch (the head branch)
# In all cases: FIX_TARGET = PR_HEAD (the PR's source branch)
FIX_TARGET="$PR_HEAD"
echo "Fix target branch: ${FIX_TARGET}"

# HARD RULE: never target main directly
if [ "$FIX_TARGET" = "main" ] || [ "$FIX_TARGET" = "master" ]; then
  echo "ERROR: fix-ci refuses to commit fixes directly to '${FIX_TARGET}'."
  echo "This PR's head branch is a protected branch — something is wrong with the PR setup."
  exit 1
fi

0C: Check initial CI status

LATEST_RUN=$(gh run list $GH_FLAG --branch "$PR_HEAD" --limit 1 \
  --json databaseId,status,conclusion,name,createdAt 2>/dev/null | jq '.[0] // empty')

if [ -z "$LATEST_RUN" ]; then
  echo "No CI runs found for branch ${PR_HEAD} — CI may not have triggered yet."
  echo "Waiting up to 60s for CI to start..."
  sleep 15
  LATEST_RUN=$(gh run list $GH_FLAG --branch "$PR_HEAD" --limit 1 \
    --json databaseId,status,conclusion,name,createdAt 2>/dev/null | jq '.[0] // empty')
fi

RUN_STATUS=$(echo "$LATEST_RUN" | jq -r '.status // "unknown"')
RUN_CONCLUSION=$(echo "$LATEST_RUN" | jq -r '.conclusion // "unknown"')
RUN_ID=$(echo "$LATEST_RUN" | jq -r '.databaseId // ""')

echo "Latest CI run ${RUN_ID}: status=${RUN_STATUS}, conclusion=${RUN_CONCLUSION}"

if [ "$RUN_CONCLUSION" = "success" ]; then
  echo "CI is already green on PR #${PR_NUMBER} — no fixes needed."
  echo '{"pr": '"$PR_NUMBER"', "status": "success", "attempts": 0, "fixes_applied": []}'
  exit 0
fi

---

Phase 1: CI Fix Loop

Initialize tracking state:

ATTEMPT=0
FIXES_APPLIED="[]"
LOOP_STATUS="pending"

cd "$REPO_PATH"
git fetch origin

Execute up to `MAX_ATTEMPTS` iterations:

while ATTEMPT < MAX_ATTEMPTS:
    ATTEMPT += 1
    echo "=== fix-ci: Attempt ${ATTEMPT}/${MAX_ATTEMPTS} ==="

    → Phase 1A: Wait for CI to finish
    → Phase 1B: Check CI result — if green, exit loop with success
    → Phase 1C: Diagnose failure
    → Phase 1D: Apply fix
    → Phase 1E: Format, commit, push
    → Loop back (CI re-triggers on new commit)

if loop exits without success AND ATTEMPT == MAX_ATTEMPTS:
    → Phase 2: Report max-attempts failure

Phase 1A: Wait for CI

Poll the latest run on the

Read more
Ships withforgedock

ForgeDock — autonomous AI dev pipeline for Claude Code. GitHub as agent memory: issue in, PR out.

Get the whole plugin

Other commands on forgedock.