Skip to content

preflight

preflight

standalone

Problem it solves — The make-green loop — run ci:check, fix, re-run, then chase mutation survivors — is the noisiest churn in a session and it floods your context. This runs the whole loop in an isolated fork and hands back one verdict, fixing the mechanical failures itself and flagging only the real decisions, so the thrash never reaches your thread.

Preflight (clear it for PR)

The make-green loop — composer ci:check, then the mutation gate — is the noisiest, most context-burning churn in a session: run, fix, re-run, fix again, especially for mutation survivors. Preflight runs that whole loop in an isolated fork and hands the main thread one line — PASS or BLOCKED. The maintainer's thread never sees the thrash.

Contract

  • Task class: standalone — drives a change to PR-ready/green, then reports a verdict (it acts; it is not a read-only audit).
  • Allowed tools: Bash (composer / php artisan / git / npm), Read · Edit · Write · Grep · Glob, codegraph, GitHub MCP (actions_*, get_job_logs, pull_request_read).
  • Direct vs fork: fork (context: fork) — the entire loop runs in a subagent on the same branch/worktree, so fixes are real edits to the working tree but the run-fix-rerun context stays off the main thread. Only the verdict returns.
  • Verifiable artifact: local composer ci:check green AND the mutation (diff-scoped) gate ≥ 95% covered-MSI on the branch — or a BLOCKED report naming the exact blocker.

The two gates (in order)

Gate 1 — local composer ci:check

Run it as TEMPO_PREFLIGHT=1 composer ci:check — a bare composer ci:check is denied in-thread by the preflight-redirect hook (it routes the make-green loop here); the sentinel is how preflight's own run is let through. On failure, fix the mechanical ones (below) and re-run. This gate is fully local — rector:check → lint/format/types → analyse → security → coverage ≥ 80.

Gate 2 — the CI mutation gate

Only once Gate 1 is green. Push the branch, then trigger the mutation (diff-scoped) workflow via workflow_dispatch — no PR needed, it's branch-runnable (CLAUDE.md). Read survivors from the job logs and kill each by adding a test, applying the qa-mutants discipline — do not restate it; follow docs/agents/mutation-testing.md (kill-by-default, strict-type expectations for DTO / casts() mutants, reason-ignore only genuine equivalents). Re-trigger to verify. Local fallback to iterate on one survivor without waiting on CI: php artisan mutate:diff --no-parallel (memory-safe; the block hook denies parallel mutation).

The heart — fix-or-flag, and the hard cap

This is the skill. Every failure is either MECHANICAL (fix it, loop, stay quiet) or a DECISION (stop, report BLOCKED). Getting that line right is the whole value: fix too aggressively and you make a product/architectural call that wasn't yours; flag too eagerly and you hand the churn straight back, defeating the point.

  • Mechanical → fix autonomously: Pint/format, Rector, type errors, a coverage gap (add the test), a mutation survivor (add the killing assertion), a flaky/infra failure (re-run once).
  • Decision → STOP + BLOCKED: a failure needing a product/architecture choice; a mutant that reveals a genuine production bug (changing prod code is the maintainer's call — name it); a survivor only killable by an @pest-mutate-ignore you're not sure is a true equivalent; anything touching a protected surface (config/harness.php protected_paths — Todoist/Google/Push/credentials, models, migrations, prompts, auth); a security finding; a test you can't write without guessing intent.

Hard cap: 5 loops per gate (override --max-loops=N). Hit the cap still red → STOP, BLOCKED with the residual. Three identical failures in a row = circuit-breaker, stop early. Never grind past the cap.

Never move the goalposts

Never lower a threshold, ->skip() / comment-out a failing test, weaken a mutation min, or contort production code to go green (CLAUDE.md Definition of Done). Green by fixing or reverting — never by moving the bar. If that's the only path, it's BLOCKED, not PASS.

Output — the verdict (the only thing the main thread sees)

  • PASS — ci:check green + mutation ≥ 95% on the branch, pushed. One line of what was fixed (counts: "killed 3 survivors, added 2 coverage tests, 1 Pint pass") + "ready to open the PR".
  • BLOCKED — the single blocker: where (file:line), why it's the maintainer's call, the smallest next step, and what IS green so far.

Keep it skimmable — the maintainer reads one verdict, not the run.

Where it sits

  • NOT qa-mutants — that's the codebase-wide nightly survivor killer / auto-merge daemon; preflight is the per-change pre-PR gate that runs both ci:check and the diff-scoped mutation gate in isolation, reusing qa-mutants' killing discipline rather than duplicating it.
  • NOT qa-tests / qa-code / check-reasoning — those are read-only audits that surface findings; preflight acts, fixing and looping to green.
  • NOT wrap-up (session close-out) or build-from-qa-plan (PR-stack state).
  • Does not open or merge the PR — it makes the branch ready and hands back PASS; opening (auto-open) / merging stays in the normal flow, the maintainer's call.
  • Not in check-everything — it pushes + triggers CI and is change-specific, too heavy/outward for a passive health sweep (same reasoning as qa-mutants).

Triggering

  • Explicit: /preflight when you think a change is ready.
  • Auto (the preflight-redirect hook): a bare composer ci:check in the main thread is denied and redirected here — so the make-green loop runs in the fork by default, not inline. The description also auto-invokes preflight on "make this green / ready for PR", and the upstream finalisers (build-sprint EXECUTE, debug) chain into it.