/ci-triage — investigate a failing CI run
SkillMonitoring & opsDiagnose a failed CI workflow run and fix it the /snag way. A tested read-only script fetches the run, reports every failed job and step with the log region around its errors (not the post-job cleanup), and classifies each against this repo's gates.tsv, printing the banner a gate carries when its ob
Use /ci-triage — investigate a failing CI run in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add /ci-triage — investigate a failing CI run and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the /ci-triage skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by jellyrock/jellyrock in .claude/skills/ci-triage/SKILL.md and read by Ahel’s review.
Single-file workflow: prep + investigation, end-to-end on opus, in main thread, no Task delegation. The mechanical prep (Steps 1-6) produces a handoff packet that's written to .claude/handoffs/ for cross-session resume + compaction recovery + /catchup discovery. The investigation contract is in sibling INVESTIGATION.md and is followed in main thread once Step 6 completes.
Contract
Goal. Take a failing CI run and drive it from "red" to root-caused end-to-end, in one main-thread session. The mechanical prep — fetch the run, find the failed job/step, extract the failure tail, classify the category (lint-fail / build-fail / device-test-fail / docs-stale-blocking / language-coverage-fail), and assemble initial file context — feeds the investigation contract in sibling INVESTIGATION.md. It runs on Opus because classifying an unfamiliar failure tail and mapping it to the right code area is genuine diagnostic judgment over real build/test output, not template fill — a misclassified failure sends the fix the wrong way. It is dedup-first: a CI run-id is immutable, so a recent unchanged triage on the same run-id (cited files untouched) short-circuits to the existing handoff. Reach for it when a CI workflow failed on a PR or on main.
Inputs. The arguments are the required run ID or URL (e.g., 1234567890 or a full /actions/runs/<id> URL — extract the run-id from /runs/(\d+)); if empty, prompt or list recent failures with gh run list --status failure --branch <current> --limit 5. The skill reads the run and its failed-step logs via gh run view, the codebase and recent diffs via git, and any prior handoff at .claude/handoffs/ci-<run-id>-*.md for the dedup check.
Outputs. A handoff packet written to .claude/handoffs/ci-<run-id>-<timestamp>.md with YAML frontmatter (created, target, branch, sha, cited-files) and a body carrying the classification, failed step, 2-5 cited initial-context files, and the failure tail (last 50-100 lines of the failed step) — durable for cross-session resume, compaction recovery, and /catchup discovery; a one-line confirmation of the saved path, classification, failed step, and file count; and then the in-thread investigation per INVESTIGATION.md. On a clean dedup hit, no new file is written — the prior handoff is surfaced with resume/re-triage/cancel options.
Success criteria.
- The dedup check runs first and short-circuits correctly: both signals clean (cited files untouched, working tree clean for them) → surface the prior handoff and STOP for the user's pick; any signal changed → proceed.
- The failed job and step are correctly identified (walk
.jobs[]/.steps[]forconclusion == "failure"); in-progress runs and succeeded runs are surfaced and stopped on, not triaged. - The failure is classified by step name first, then by failure-tail content when the step name doesn't match a known shape; genuinely unmatched failures classified
unknownfor the investigator. - The failure tail keeps the diagnostic region (last ~50-100 lines plus extra context above a stack trace) and drops the setup boilerplate.
- The handoff is written with valid frontmatter, then the skill continues immediately into
INVESTIGATION.mdas one motion.
Failure modes to avoid.
- Re-prepping over a clean dedup hit. If both signals are clean, do not write a new file — surface the prior handoff and wait for the user's pick.
- Triaging an in-progress or succeeded run. Wait for completion;
/ci-triageis for failures only. - Capturing the wrong log region. Drop the workflow-setup/checkout/npm-install boilerplate; keep the diagnostic tail (and 5-10 lines above a named error or stack trace).
- Forcing a classification. When neither the step name nor the tail content matches a known shape, classify
unknownrather than guessing wrong. - Triaging transient infra failures. If the failure is a GitHub Actions outage / runner-unavailable, re-run first; only triage code if it repeats.
- Stopping after the handoff. Step 7 is "save the handoff AND continue into investigation" as one motion; don't write the file and wait.
When NOT to use.
- The run is in-progress — wait for it to complete.
- The run succeeded — there's nothing to triage.
- The failure is a transient infra issue (Actions outage, runner unavailable) — re-run first; triage code only if it repeats.
- The pasted text is a Roku runtime log, not a CI log — use
/runtime-triage. - The failure is a docs-stale-blocking whose fix is purely mechanical (bump
last-reviewed, no shape change) — fix directly without the investigation contract.
Implementation
Inputs
$ARGUMENTS: required run ID or URL (e.g., 1234567890 or https://github.com/jellyrock/jellyrock/actions/runs/1234567890). If empty, prompt or list recent failures: gh run list --status failure --branch <current> --limit 5.
If the input is a URL, extract the run-id (/runs/(\d+)).
Step 0 — Check for prior triage (dedup)
Before any prep, look for a recent handoff on this run-id:
ls -t .claude/handoffs/ci-<run-id>-*.md 2>/dev/null | head -1
If a prior handoff exists, Read it. The handoff has a YAML frontmatter with created, branch, sha, cited-files. The run-id itself is immutable (CI runs don't change), so check two signals:
- Cited files unchanged?
git log <sha>..HEAD -- <cited-files>— empty output means no commits touched them on this branch. - Working tree clean for cited files?
git status --porcelain -- <cited-files>— empty means no uncommitted changes.
If both are clean, do not write a new file. Surface to the user:
Prior CI triage exists at
.claude/handoffs/ci-<run-id>-<timestamp>.mdfrom . Cited files unchanged since then. Options:
- (a) Resume from the existing triage — Read the handoff and follow
INVESTIGATION.mdfrom there- (b) Re-triage anyway — fresh prep (use this if a re-run of the workflow produced different output)
- (c) Cancel
Then STOP. Wait for the user's pick before proceeding.
If any signal shows change (or no prior handoff exists), proceed to Step 1.
Step 1 — Fetch the run
gh run view <run-id> --json status,conclusion,name,event,headBranch,jobs,createdAt,htmlUrl
If the run is still in-progress, surface that and stop — there's nothing to triage yet. If the run succeeded, surface that too — /ci-triage is for failures.
Find the failed job(s) by walking .jobs[] and filtering conclusion == "failure". Each job has .steps[] — find the failed step (also conclusion == "failure").
Step 2 — Extract the failure tail
gh run view <run-id> --log-failed --job <job-id>
This streams the failed step's log. Capture the last ~50-100 lines — that's where the diagnostic message lives. Drop the leading boilerplate (workflow setup logs, git checkout, npm install) — the user doesn't need them.
If the failure shows a stack trace or named diagnostic, capture 5-10 extra lines of context above the error line.
Step 3 — Classify
Match the failed step's name + the failure tail to a category:
| Failed step name | Category |
|---|---|
lint, lint:*, check-formatting, validate, format:check | lint-fail |
build, build:* | build-fail |
test:tdd, test:unit, test:integration, test:all, device-tests | device-test-fail |
lint:docs, docs:check, docs-stale* | docs-stale-blocking (or lint-fail if it's a docs-check broken-ref, not a stale-blocking) |
lint:language-coverage, language-coverage | language-coverage-fail |
lint:translations | lint-fail (translations subcategory) |
If the step name doesn't match a known shape, use the failure-tail content to classify:
BRIGHTSCRIPT_ERR_*, BSC errors → likelybuild-failorlint-fail[Rooibos Result]: FAIL→device-test-failstale doc ... related-files→docs-stale-blocking- ESLint output →
lint-fail
If still ambiguous, classify as unknown — the investigator will sort it out.
Step 4 — Identify probable area
The failed step often points at the area:
lint:bs/validate/buildfailures: the BSC error names file:line. The file path → area mapping is the same as/runtime-triage's "Identify probable area" step (usespkg:/components/...orpkg:/source/...).lint:docsfailures: the validator's stdout names the broken doc + path.device-test-fail: the Rooibos output names the test file (tests/source/unit/<area>/...).docs-stale-blocking: names the stale architecture doc.
Step 5 — Assemble initial file context
For the probable area, surface 2-5 files:
# Recent commits scoped to the failing file's area — regressions often
# correlate with a recent change
git log --oneline -10 -- <file-or-area>
# Diff between this branch and main (what's NEW vs the baseline)
git diff main...HEAD -- <file-or-area>
For test failures, include the test file + the SUT it tests.
Step 6 — Build the handoff packet
Construct the packet with a YAML frontmatter (so future Step-0 dedup checks can read it) plus the prep body:
---
created: <ISO-8601 UTC timestamp from `date -u +%Y-%m-%dT%H:%M:%SZ`>
target: ci-<run-id>
branch: <git rev-parse --abbrev-ref HEAD>
sha: <git rev-parse --short HEAD>
cited-files:
- <path-1>
- <path-2>
---
CI failure
Run: <run-id> (<workflow name>)
Branch: <branch>
Triggered by: <event>
URL: <htmlUrl>
Classification: <category>
Failed step: <step name>
Initial file context:
- <path>:<line range or whole-file> — <one-line why-relevant>
- ...
Failure tail (last 50-100 lines of the failed step):
<log excerpt>
Step 7 — Write the handoff and continue into investigation
-
Compute the timestamp:
date +%Y%m%d-%H%M%S(filename) anddate -u +%Y-%m-%dT%H:%M:%SZ(frontmatter). -
Write the packet to
.claude/handoffs/ci-<run-id>-<YYYYMMDD-HHMMSS>.md. -
Output a single confirmation line, this exact shape:
Handoff saved:
.claude/handoffs/ci-<run-id>-<timestamp>.md(classification: , failed step: , files cited). Now followingINVESTIGATION.md— adjust scope freely. -
Then continue immediately into the investigation contract at sibling
INVESTIGATION.md. Don't stop or wait.
Sub-agent invocation
To invoke from a parent sub-agent (rare): parent passes Read .claude/skills/ci-triage/SKILL.md and follow Steps 0-7 for $ARGUMENTS=<run-id>; write the handoff file but stop before INVESTIGATION.md — surface the handoff path so the parent can decide next. End your report with a "Captures for /log" section: one "- <type>: <title> — <body>" bullet per journal-worthy item this work surfaced, where <type> is decision, followup, signal, or running (a signal is an upstream version-watch row; a running item replaces the one-paragraph note on what is being worked on right now); omit the section if there are none, and never write to journals yourself. in the Task prompt. Sub-agents only run the prep; they don't follow INVESTIGATION.md (which is interactive).
Signals
- GitHub stars
- 45
- Forks
- 2
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
ci-triage-jellyrock- Source
- github.com/jellyrock/jellyrock
github.com/jellyrock/jellyrock
Related picks
Skill · tddworks
The pick for GitHub Actionsgithub-actions-docs
Skill · devantler-tech
The pick for GitHub Actionsinternal-comms
Skill · anthropics
More in Monitoring & opsagent-eval
Skill · affaan-m
More in Monitoring & opspricing
Skill · coreyhaines31
More in Monitoring & opslark-okr
Skill · larksuite
More in Monitoring & ops