The workflow, step by step
workflow.yaml is the source of truth for order: 15 steps, each with prerequisites, exit predicates and a closed list of blockers. The skills are prose about the same pipeline; when they disagree, the YAML wins because the YAML is what the gate reads.
The DAG
Arrows mean "must be closed (or blocked) first". After build the pipeline fans out into four independent checks and joins again at publish-preview.
Numbers are the order status lists them in. Shaded boxes are the joins: build gates four checks, publish-preview needs three of them back.
Two profiles exist. site runs everything. app excludes scaffold and flows — an application brings its own build and its Playwright suite replaces the frozen flow.
How a step is written
Every step has the same five keys. Here is mwg-install, the shortest one that shows all of them:
- id: mwg-install
title: Modern Web Guidance is installed and pinned
scope: site
actor: orchestrator
requires: [repo]
exit:
- kind: file
glob: ".agents/skills/modern-web-guidance/SKILL.md"
min_count: 1
- kind: json_field
file: "skills-lock.json"
pointer: "/skills/modern-web-guidance/version"
why: "an unpinned skill silently changes what 'passed' means"
blockers: []
The why lines are for the reader, not the gate — but they are the best short explanation of each check, and the tables below quote them.
Setup: steps 1–4
| Step | Actor | Closes when | Artefact and its author |
|---|---|---|---|
| 1 preflight The host can finish this build | orchestrator | doctor.mjs exits 0: Node ≥ 22.12, Chrome found, gh and wrangler name an account, operator attestation under a year old. | preflight.json, written by the doctor even on failure — so the preflight-failed blocker has evidence to stand on. |
| 2 brief One action, an audience, proof | site-brief | brief.md exists; required fields non-empty; exactly one primary action. | brief.md, by the skill's interview. |
| 3 repo A private repo with an initial commit | orchestrator | Site dir is a repo root, a commit exists, a remote is set, and gh reports it private. Unconfirmable is refused. | The repo, by git and gh. |
| 4 mwg-install Guidance installed and pinned | orchestrator | The skill file exists under .agents/skills/ and skills-lock.json pins its version. | mwg-install.mjs install, which vendors the guidance and writes the lock with a tree hash. |
Design and build: steps 5–7
| Step | Actor | Closes when | Artefact and its author |
|---|---|---|---|
| 5 design A per-site design system, contrast holds | design-system | site-context.json names the Claude Design project (Claude Design is Anthropic's design tool; a project there is the source the tokens are exported from — tokens come before the scaffold because the scaffold consumes them); that project was looked up since the tokens last changed; src/styles/tokens.css exists; every declared text/background pair reaches 4.5:1 in both colour schemes. No large-text escape. | tokens.css by the skill; the pairs and project id via site-context.mjs design. |
| 6 scaffold The project builds | site-scaffold | npm run build exits 0. | The Astro project; routes declared via site-context.mjs routes. |
| 7 build Pages implemented, guidance consulted, no runtime errors | orchestrator | The ledger proves a guidance search before every component's first write, and nine tools pass on every declared route: axe-routes (both schemes), a11y (skip link via real Enter and Tab, motion pausable, fragments resolve, no placeholder-only labels), console-clean, seo --build, forms (autocomplete tokens), fonts (no FOIT), bfcache (one real back-navigation restored), markup, content (text walls, DE+EN spelling, readability ratchet, article dates). | The pages, by the agent; the ledger, by the hooks. |
Step 7 is where most refusals happen, and each predicate prints a line per finding. Treat the FAIL lines as the to-do list and re-run done build until it closes.
Checks: steps 8–12
| Step | Actor | Closes when | Artefact and its author |
|---|---|---|---|
| 8 security The built site defends itself | orchestrator | security.mjs: 19 checks on _headers and dist/ — CSP without unsafe-inline, HSTS, nosniff, referrer and permissions policies, no secrets, SRI, no sourcemaps, and more. deps.mjs: no high or critical advisory. supply-chain.mjs: lifecycle scripts off, lockfile URLs on the public registry, signatures verified. | _headers, by the scaffold. Reports "declared, not proven live" — never "secure". |
| 9 flows The critical flow works and is frozen | orchestrator | A flows/*.cli.mjs script exists and replays green against the predicate's own server. Any budget line it prints is judged: INP ≤ 200 ms, zero layout shift, a busy state within 300 ms of any wait over 300 ms. | The flow script, by the agent using flow-kit.mjs. |
| 10 ux-review Nothing manipulative ships | ux-review | ux-review.json gives a verdict with a ≥ 20-character reason for all 31 mandatory patterns, carries the catalogue fingerprint, covers every declared route, is newer than the build, and a scanner finds no signature the review calls absent. | ux-review.json, by the skill. Corroborated, not trusted. |
| 11 commercial-review Someone would pay for this | commercial-jury | Three distinctly named blind scorers — separate agent sessions or people that have not seen the brief or each other's scores — the identical 11-dimension rubric, integer scores 1–5, median per dimension ≥ 3, filed after the last change; plus an owner sign-off. | Ballots via jury.mjs record; sign-off via site-step sign. |
| 12 ship-check The floor holds, measured not asserted | orchestrator | A recorded measurement: 5 Lighthouse runs, median within budgets.json, spread ≤ 5 points. | measure.mjs, which refuses fewer runs, a wide spread, or a Lighthouse other than the pinned 12.2.1. |
Publish: steps 13–15
| Step | Actor | Closes when | Artefact and its author |
|---|---|---|---|
| 13 publish-preview Reviewable at a URL, invisible to search | site-publish | The preview URL is observed to send x-robots-tag: noindex and a content-security-policy — a declared header the host silently drops is what step 8 cannot see. Every licensable asset in dist/ maps to a licences.json entry with source, licence and proof. | Headers recorded by publish.mjs probe; URL via site-context.mjs urls. |
| 14 legal-check Required legal elements present | legal-check | legal.mjs: 23 checks on the live preview — Impressum by legal form, Datenschutz naming every observed and declared processor, zero cookies or storage before consent, reject as prominent as accept and re-observed, no fonts.gstatic.com, BFSG, AI Act, PAngV, § 312k. Plus a legal-reviewer sign-off. | Declarations via site-context.mjs hosting|processor|bfsg|legal|jurisdiction. Reports "present" — never "compliant". |
| 15 publish-live Launched on its own domain | site-publish | live.mjs: DNS resolves, noindex gone, sitemap and robots served, and the receipt is newer than the newest file under src/, public/, dist/. seo.mjs: self-referential canonicals, Open Graph, JSON-LD, every link resolves. dns.mjs: SPF, DMARC and MX match the declared mail posture, no dangling CNAME, sane CAA, registration not lapsing. | The receipt, by verify-receipt.mjs audit; mail posture via site-context.mjs mail. |
Blockers: the only legal way around a step
A blocker (that is: a recorded, validated reason a step cannot be done) is the machine's escape hatch, and it is narrow by design. Each step lists which blockers it accepts; eight steps accept none and can only be done.
| Blocker | On step | Validator requires |
|---|---|---|
preflight-failed | preflight | preflight.json records pass: false |
design-source-external | design | --evidence pointing at an existing file (the agreed style guide) |
mwg-not-applicable | build | --evidence: the written file that is genuinely not a component |
advisory-unfixable | security | --evidence: which advisory, and why the exposure does not apply |
flow-not-applicable | flows | --evidence: the site genuinely has no interactive flow |
no-ai-feature | legal-check | site-context.json does not declare an AI feature |
no-custom-domain-yet | publish-live | --evidence: the domain is not registered or delegated yet |
Every blocker also needs --reason of at least 20 characters. A blocked step counts as satisfied for the steps after it, and shows as BLK in status. A refused blocker halts the run like any other refusal.