ci-watch · git:20260812.21e8bdf · 2026-08-12 · sha256 2f959054ae250cce

ci-watch git:20260812.21e8bdfA

Immutable. This exact content never changes and is served at /api/v1/blob/2f959054ae250cce.

---
name: ci-watch
description: Watch the GitHub Actions run for a pushed commit and repair the build if it goes red — read the failing job logs, diagnose, fix, commit, push, and keep going until CI is green. Use after a push, when asked "is CI green", "did the build pass", "watch the CI", "fix the build", or when a push-landed tripwire hands over a SHA. Also covers what does NOT count as a red build (cancelled runs, Actions outages, transient gh errors).
allowed-tools: Bash(.claude/skills/ci-watch/scripts/watch-ci.sh:*), Bash(gh:*), Bash(git:*)
---

# Watch the CI, and fix it when it breaks

`scripts/watch-ci.sh [sha]` blocks until the `ci.yml` run for a commit reaches a
verdict, then **encodes that verdict as its exit status**. The SHA defaults to `HEAD`.

```sh
.claude/skills/ci-watch/scripts/watch-ci.sh              # HEAD
.claude/skills/ci-watch/scripts/watch-ci.sh 3c7c5cf9     # a specific commit
```

**Always launch it as a background Bash task** (`run_in_background: true`) and keep
working. It polls for minutes; blocking on it wastes the whole wait. The harness
re-invokes you when it exits.

## The three verdicts

| Exit | Output | What you must do |
|---|---|---|
| `0` | `CI passed for <sha>` | Report it. Done. |
| **non-zero** | `CI FAILED (<conclusion>)` | **Repair the build.** See below. |
| `0` | anything starting `⚠️` | **Not** a verdict. Do *not* repair, do *not* claim green. |

That third row is the one that matters, and it is why the exit status is not simply
`gh run watch`'s. A **false red is far more damaging than a false green**: it sends
you off "fixing" a perfectly healthy build and pushing changes nobody asked for. So
the script refuses to report red unless GitHub gives an authoritative failing
conclusion. These are all `⚠️` non-verdicts, never red:

- **`cancelled`** — almost always a newer push superseding the run (concurrency
  cancel), or a manual cancel. Not a build failure.
- **No job ever started** (no runner assigned, zero steps) — an Actions capacity or
  outage problem. Zero steps ran, so no gate evaluated the code; there is nothing
  to repair.
- **Empty or unknown conclusion** — a transient `gh` problem (401, network, rate
  limit), not a build result.
- **`ci.yml` never started** within ~2 min — the commit is simply *unverified*. Not
  green, not red. Re-run the script later, or `gh workflow run ci.yml`.

`ci.yml` is the only gate that counts. A push fires several workflows
(`pages-mirror`, `diagram-preview`, GitHub's implicit `pages-build-deployment`), and
an unscoped `gh run list` returns whichever registered first — that produced a real
**false green** on 2026-08-06, reporting success off `pages-build-deployment` while
`ci.yml` had not started at all. Every lookup in the script is scoped to `ci.yml`
on purpose. Do not "simplify" that away.

## When it goes red

Repair it **automatically — do not ask for permission**, and repair it **even if
somebody else broke it**. Any red build you catch is yours to fix.

1. **Read the actual failure**, don't guess:
   ```sh
   gh run view <run-id> --log-failed
   ```
2. **Diagnose the root cause.** A failing assertion is a real signal — fix the code,
   not the test, unless the test is provably wrong. Do not delete, skip, or loosen a
   test to get green; that is defeating the gate, not repairing it.
3. **Reproduce locally when it's cheap** (`mvn test -Dtest=...`, `npm test`). Beware
   the traps in the project's CLAUDE.md — never run `mvn test` while IntelliJ is
   building, and remember `-Dtest=...` makes coverage look falsely low.
4. **Fix, commit, push.** The push re-arms the tripwire, which hands you a fresh SHA —
   so a new watch starts on its own. Don't hand-roll a second watch for the same push.
5. **Loop until green**, reporting what you changed each round.

If two rounds fail to move the failure, stop and surface it rather than pushing a
third speculative fix — a build that resists two informed attempts usually needs a
decision, not another commit.

Bear in mind the Sonar gate in `ci.yml` is what actually fails builds on coverage
(`new_coverage` 80%), and a local `mvn test` never checks it — so "tests pass
locally" is not evidence the build is repaired.

## How this gets triggered

`.claude/hooks/watch-ci-after-push.sh` is a `PostToolUse` tripwire on Bash. It does
**not** parse your command to decide whether you pushed; it reads the reflog on the
upstream remote-tracking ref, which is authoritative — a push from the *other*
petclinic checkout never touches this repo's reflog — and it de-dupes per delivered
SHA, so several Bash calls after one push start exactly one watch.

The tripwire hands over the SHA and the one-line command; it deliberately does **not**
inline this repair protocol, because the green path is the common one and does not
need it. Consult this skill when the watch comes back red, or when driving a watch
by hand.