How a run moves · 7

Life-cycle of a run

A run is a run-state.json file and the commands that move it. This page follows one from its first byte to a live site, and stops at each thing that can go wrong on the way.

Step states

Each step in the state file is in exactly one of four states. The gate computes a fifth — open — from the others.

Statestatus showsSet byMeaning
openblankcomputedEvery prerequisite is done or blocked; next lists it.
in progress...startSomeone is on it; the active-run pointer names this site.
doneOKdoneEvery exit predicate passed at closedAt; the predicate results are stored.
blockedBLKblockA blocker from the step's vocabulary was validated; reason and evidence stored. Counts as satisfied downstream.

start is optional. done re-checks the prerequisites itself, so skipping start is a convenience, not a bypass — but without it a file written before the first done may not be attributed to the run.

The state file

{
  "site": "example",
  "profile": "site",
  "specVersion": 1,
  "halt": null,
  "steps": {
    "preflight": { "status": "done", "startedAt": "…", "closedAt": "…", "actor": "orchestrator",
                   "predicates": [ { "kind": "tool", "ok": true, "detail": "preflight ok — 5/5" } ] },
    "brief":     { "status": "done", "…": "…" },
    "repo":      { "status": "in_progress", "startedAt": "…" }
  }
}

Only start, done, block and sign write it. status and next never do, and the predicates never do either — a predicate run by hand leaves no trace.

Halts

A halt is a field in the state file, not an exit code. When done finds a failing predicate, when a prerequisite is missing, or when a blocker fails its validator, the gate writes:

"halt": { "step": "build", "kind": "exit_predicate",
          "reason": "cannot close build: 2 exit predicate(s) failed",
          "details": ["console-clean: 1 failed request on /about (favicon.ico 404)", "…"],
          "at": "2026-08-29T10:02:17.551Z" }

While it stands:

A halt is lifted by returning to the halted step: start on it, done closing it, or a block surviving its validator. The output then includes halt lifted — …. Lifting is cheap on purpose — it is a stop sign, not a lock. What it protects is the order: the failing predicates are still there the next time done runs, and nothing after the step opens until they pass.

Sign-offs

Two steps carry a signoff predicate: commercial-review (role owner) and legal-check (role legal_reviewer). sign <site-dir> <step> --by "Name" records the person, the role and a ledger line, and prints a reminder to run done. It closes nothing, because a human approval is one predicate among several — the jury medians or the 23 legal checks still have to hold.

Freshness

Three records describe a specific build and become worthless when the build changes: the ux-review, the jury, and the receipt. Each is compared against newestChange — the newest modification time under src/, public/ and dist/, ignoring node_modules, .git, .astro and .DS_Store.

What freshness does

Edit a page after the review, and done ux-review refuses again the next time it is run. The review must be re-filed against the build that is there now.

What it does not do

Nothing re-opens a closed step by itself. A step stays OK in status until something re-runs done on it — publish-live does, through live.mjs, for the receipt.

<site>/.claude/state/ is excluded from the scan on purpose. Otherwise writing a receipt would make the site look newer than the receipt, and nothing could ever ship.

Receipts and waivers

A receipt is the file that says the quality bar was measured, not asserted. Only scripts/verify-receipt.mjs audit writes it, and only after a real Lighthouse run — pinned version, mobile emulation — grades within budgets.json. The scores in the receipt are parsed from the raw report saved next to it, and check re-reads that report: a receipt whose numbers differ from its report is refused. Faking one would mean fabricating a full Lighthouse report.

Three commands:

CommandDoesSuccess looks like
audit <site-dir> <url>Runs Lighthouse, grades, writes receipt.json on pass.A scorecard and PASS — receipt written. Safe to claim done.
check <site-dir>Re-validates without a browser: report still meets budget, receipt matches report, under 24 h old.No output, exit 0.
waive <site-dir> "why"Records a 24-hour deferral with a reason. Only check — the receipt re-validation — honours it; step 12's measurement does not.Waived ship-check for <site> for 24h — reason: …

The receipt is separate from step 12's measurement. measure.mjs records five runs and their spread into the state file for the gate; verify-receipt audit writes the receipt that publish-live reads. Both call the same Lighthouse and the same grading code.

Declarations

Some facts no program can discover: whether the domain sends mail, who the hosting processor is, which jurisdiction applies, whether the site is in BFSG scope, what legal form the owner has. The machine asks for them once, through site-context.mjs, and validates them where they are read. A missing declaration fails closed — dns.mjs refuses to judge SPF without a mail posture, rather than assuming one.

After launch

reverify.mjs --sites-root <dir> re-runs the live predicates against every published site, monthly, out of band. It writes its reports under <site>/.claude/state/reverify/ and never touches run state, halts, or the active-run pointer. Exit 1 means drift or an unreachable site.

Self-check: the two ways a run stops

A halt and a blocker both stop forward motion. Which one is a state the gate raises, which one is a state you request — and which of the two lets the steps after it proceed?

One run, end to end

A worked example that touches every part. The site is a one-page portfolio for a photographer, ~/sites/lena, built by an agent in one afternoon with a person watching.

  1. Preflight. done preflight runs the doctor: 5/5. preflight.json written. The SessionStart hook will now announce this run at every session start.
  2. Brief. The site-brief skill asks who the site is for and what the one action is — "book a shoot". brief.md written; brief-complete.mjs finds one primary action. Closed.
  3. Repo, guidance. gh repo create --private; mwg-install.mjs install vendors the guidance and pins 2026_05_16-c5e78707. Two steps closed.
  4. Design. The skill writes tokens.css and declares four pairs. contrast.mjs fails one: the muted caption colour is 4.1:1 in dark mode. Halt. The agent darkens the token, runs done design again, halt lifted.
  5. Scaffold, build. Astro builds. The agent searches the guidance for "image gallery" and "dialog" — two mwg_search lines in the ledger — then writes Gallery.astro and Booking.astro. done build: the ledger claim holds; console-clean fails on a missing favicon.ico. Halt, fix, closed.
  6. Security, flows, ux-review, ship-check — the fan-out. _headers gets a CSP; deps.mjs is clean. The booking flow is frozen as flows/book.cli.mjs and replays green. The ux-review skill files 31 verdicts with the fingerprint. measure.mjs: median 96, spread 3. Four steps closed.
  7. Commercial review. Three blind scorers — three fresh agent sessions given only the preview URL — file ballots; jury.mjs record computes medians, the lowest is 3. Lena signs: sign … commercial-review --by "Lena". Then done. Closed.
  8. Publish preview. wrangler pages deploy — the guard lets it through, no halt stands. publish.mjs probe records noindex and the CSP from the real response. licences.json covers the 12 photos. Closed.
  9. Legal check. legal.mjs finds the Datenschutz does not name Cloudflare, which site-context.mjs hosting declared. Halt, text fixed, redeployed, re-observed. A reviewer signs. Closed.
  10. Publish live. Lena edits one caption before launch. verify-receipt audit is run again because live.mjs would refuse the older receipt. dns.mjs checks the parked-domain SPF/DMARC trio, since the site declared it sends no mail. done publish-live: closed. status: 15 of 15.

Four halts, none of them a judgement call, each lifted by evidence. The agent never marked anything done; it only made the predicates true.