First run

Start offline. Passing a log path instead of a run id runs the whole pipeline against a captured log with zero network and zero LLM — the fastest way to see what the tool actually does.

Replay a saved log

# Save the log of a job that failed, then:
ci-doctor analyze failing-job.log

An existing path means no fetch is attempted, so this needs no token, no config and no model. It is also the fastest way to test a .ci-doctor.yml or reproduce an attribution you disagree with.

Against a live run

# Against a live run (reads $CI_PIPELINE_ID / $GITHUB_REPOSITORY in CI):
ci-doctor analyze "$CI_PIPELINE_ID"

# From your laptop, with no CI variables set, the repo comes from your
# git origin — and it logs a warning naming what it picked:
ci-doctor analyze 123456

This one needs a read-scoped token — see Requirements .

What you get

╭─ ci-doctor ─────────────────────────────────────────────────╮
│ build failed in the script phase (script_failure).          │
│ phase=script  category=test  confidence=high                │
╰─────────────────────────────────────────────────────────────╯
Root cause
  test_add asserted 1 == 2
Evidence
  ╭─ script ────────────────────────╮
  │ E   assert 1 == 2               │
  ╰─────────────────────────────────╯
Remediation
  1. Inspect the failing assertion in tests/test_math.py
No model configured? That is a supported mode, not a degraded one. Withllm.enabled: false — or simply llm.model unset — you still get the phase, the reason, the terminal command, the evidence and remediation steps. The model only ever adds the prose explanation.

When the verdict looks wrong

Run it again with -v. The denoise → extract → fit line shows how many lines each stage kept and whether the budget had to truncate, and the attribution line names the exact rule that fired. Both are covered onthe CLI reference , andSections & attribution  explains what those rule names mean.

Happy with it? Wire it into your pipeline →