# Fix Until Green

> Drive a failing build to passing in a disciplined loop: run the project's full gate (tests + lint + typecheck), fix one root cause, re-run the whole gate, and repeat until all three exit clean — with a hard iteration cap, no-progress detection, and a rule against cheating the gate green. Use when tests/CI are red and you want them green, when a build wave fails its checks, or as the QE inner loop under an orchestrated build. Trigger on: "fix until green", "make the tests pass", "get CI green", "fix the failing tests", "make it green", "drive the build to passing", "red to green", "loop until tests pass", "fix lint and type errors", "keep fixing until the suite is clean", "make the wave gate pass". A configuration of loop-controller — it supplies the harness; this skill supplies the proof (three exit codes) and the fix discipline.

- Skill: `ivy00johns/fix-until-green` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add ivy00johns/fix-until-green`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ivy00johns/fix-until-green/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ivy00johns (https://skillmd.com/u/ivy00johns)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ivy00johns/fix-until-green

---


# fix-until-green

> **A configuration of [`loop-controller`].** That skill supplies the loop
> machinery — primitive selection, the full guardrail stack, state
> externalization. This skill supplies the two things specific to "get the build
> green": a **mechanical proof** (three exit codes) and the **fix discipline**
> that keeps the loop from cheating its way there. Read `loop-controller` for the
> guardrails; they're inherited, not repeated here.
>
> **Why `disable-model-invocation`:** this loop edits and commits code on its own
> until a gate passes. You want to *type* `/fix-until-green` (or have the
> orchestrator dispatch it) — not have Claude start an autonomous edit loop
> because a test happened to fail.

## The 5-part contract

| Part | This loop |
|---|---|
| **trigger** | red suite / CI, a failed build wave, or an explicit `/fix-until-green` (optionally scoped to a path) |
| **action** | run the whole gate → read the highest-signal failure → fix **one root cause** → re-run the **whole** gate |
| **proof** | `test` exits 0 **AND** `lint` exits 0 **AND** `typecheck` exits 0 — default-FAIL (assume red until all three are observed clean **in the same run**) |
| **memory** | `fix_plan.md` (live TODO of remaining failures + the failure→fix log) once the loop runs past one iteration or has to escalate — a single trivial mechanical fix doesn't need the ceremony; plus a git checkpoint per green-ward step |
| **stop** | all three clean **OR** iteration cap **OR** no-progress for 3 rounds **OR** budget cap |

## The proof: three exit codes, default-FAIL

"Green" is not "the test I was looking at passes." It is **all three of test,
lint, and typecheck exiting 0 in the same run**, with no test removed or weakened
to get there. Assume **red** until you have observed all three clean together —
that's the default-FAIL stance. A loop that calls it done after fixing the one
visible failure ships the other two broken.

Some stacks fold lint/typecheck into the test command or omit one (a dynamically
typed project may have no typecheck step). Detect what actually exists (Step 1);
a step that genuinely doesn't exist passes vacuously, but **don't assume** —
absence must be confirmed, not guessed.

## Step 1 — Detect the gate commands

Find the three commands for *this* project before looping. Precedence:

1. **`.claude/profile.yaml`** if present — it may declare `test` / `lint` /
   `typecheck` explicitly. Use those verbatim.
2. **Declared scripts** — `package.json` `scripts`, `Makefile` targets,
   `pyproject.toml` / `tox.ini`, `Cargo.toml`, `go.mod`, `composer.json`. Prefer
   the project's own named script (`npm run test`, `make check`) over a raw tool
   invocation, so you run what CI runs.
3. **Stack default** — only if nothing is declared. The per-stack table (node,
   python, rust, go, ruby, java, …) is in `references/gate-commands.md`.

Record the three resolved commands in `fix_plan.md` so every iteration runs the
identical gate. Running a *different* command than CI is the most common way a
"green" loop produces a still-red PR.

## Step 2 — Run the whole gate, read the real failure

Run all three commands, capture exit codes and output. Then **pick one
root-cause failure to fix** — not the first line of the stack trace, the *cause*.
Twenty failures with one shared cause is one fix, not twenty. Don't shotgun
edits across many failures in a single iteration; that destroys the signal about
what actually moved the gate.

## Step 3 — Fix one root cause

- **Mechanical failures** (a clear type error, a missing import, an off-by-one
  the assertion points straight at) — fix directly.
- **Hard failures** (flaky tests, "passes locally fails in CI," a performance
  regression, intermittent races, "I can't reproduce it") — **invoke
  [`diagnose-loop`]** rather than guessing. Its Phase-1 discipline (build a fast,
  deterministic, binary-signal reproduction first) is exactly what a fix loop
  needs and exactly what gets skipped under pressure to go green.
- **Change one variable per iteration** so the re-run tells you whether *that*
  fix worked. This is what makes the loop a binary search instead of guessing.

## Step 4 — Re-verify the whole gate, checkpoint

Re-run **all three** commands from scratch — not just the one you touched. A fix
that greens the failing test but reds typecheck has made the gate worse, and only
a full re-run catches it. This is the "restart the streak" rule: the proof is
about the *whole*, re-checked after every change.

On a green-ward step (strictly fewer failures, nothing new broken), **commit a
checkpoint** with a message naming the root cause fixed — the git trail is the
loop's undo and its post-mortem. When all three exit 0 together, the loop is
done; report the final gate output as evidence.

## Guardrails specific to this loop

Inherits the full stack from `loop-controller` → `references/safety.md`. The
caps this loop sets:

- **Iteration cap** — default ~15–25 (read from `.claude/profile.yaml` if set).
  Hitting the cap is a *stop-and-escalate*, not a license to weaken the gate.
- **No-progress detection** — if the **same set of failures** survives **3
  consecutive iterations**, stop and escalate. The same failure three times means
  the approach is wrong, not that it needs a fourth try. Surface the stuck
  failure to the human with what was tried.
- **Never cheat the gate green.** Forbidden, and each is a *finding* if you catch
  it: deleting or `skip`-ping a failing test, weakening an assertion, adding an
  ignore directive (`// eslint-disable`, `# type: ignore`, `@ts-expect-error`) to
  silence rather than fix, or relocating a violation into the checker's blind
  spot. If a test is genuinely wrong, that's a human decision — surface it, don't
  unilaterally delete it. (Anthropic's harness, verbatim: *"It is unacceptable to
  remove or edit tests."*)
- **Green against a mock is green-on-a-proxy — not the same as done.** This one
  isn't cheating (you weakened nothing) — but `test+lint+typecheck` exiting 0
  against a stubbed DB, a fake API, or a mock provider proves the *mocked* build
  is internally consistent, not that the product works. That's a real result
  worth having (it compiles, the wiring type-checks), and a perfectly good gate
  for *this* loop. It is **not** "done" for anything whose value depends on the
  real path. When that's the situation, say so out loud — "three exit codes green
  **against the mock**; the real `<X>` path is unexercised" — and route the gap to
  `qe-agent` / `coverage-loop`; don't let a green badge imply the product runs.
  See [`loop-controller`] Step 2, *measure the goal, not a stand-in*.
- **AFK-safe only within the reversible boundary.** Editing source + running the
  gate is reversible and has a hard verifier — fine unattended. If a fix would
  touch something irreversible (a migration against a real DB, an external API,
  anything destructive), that's an HITL checkpoint — pause for the human.

## Choosing the driver primitive

Per `loop-controller` Step 1, by how you're running it:

- **Default — `/goal`:** the proof is provable from command output, so
  `/goal "the test, lint, and typecheck commands for this project all exit 0 in
  the same run, with no test removed or weakened — or stop after N turns."` The
  Haiku evaluator reads the gate output you surface each turn. Remember the
  embedded turn cap — `/goal` has no native budget.
- **Stop-hook gate** when you want the check to ship *with* the build and block
  exit deterministically (the orchestrator wave gate, a per-skill gate other role
  skills inherit). The gate script + `stop_hook_active` guard are in
  `references/stop-hook.md`.
- **`claude -p` (Ralph)** for a long, greenfield, fresh-context-per-iteration run
  — sandboxed, with an external iteration/token counter.

## Using it under the orchestrator

This is the **QE inner loop** and a natural **wave gate**. When the orchestrator
dispatches it (or wires the Stop-hook into a wave), failures route back to the
owning agent **by file**: a red typecheck in `src/api/` goes to the backend
agent, not a generic "fix it." The orchestrator does **not** override a stuck
loop — if fix-until-green escalates after no-progress, that's a real blocker for
the QA gate, not a number to paper over. The relationship to the QA gate is
exactly `loop-controller`'s rule: the loop informs, the `qe-agent`'s
`qa-report.json` still decides.

## Reference files

- `references/gate-commands.md` — per-stack detection of the test / lint /
  typecheck commands (node, python, rust, go, ruby, java, php, …) and how to
  compose three exit codes into one pass/fail.
- `references/stop-hook.md` — the Stop-hook gate that ships with this skill: the
  script, the `stop_hook_active` guard, and how to wire it into a build or have a
  role skill inherit it.

[`loop-controller`]: ../loop-controller/SKILL.md
[`diagnose-loop`]: ../../workflows/diagnose-loop/SKILL.md

