Process Recipes
A separate class from agentic (task) recipes and from guides. A process recipe is a framework-specific driver for one phase of the development lifecycle — e2e setup, visual-regression setup, contrib research. The plugin owns the generic, stack-neutral mechanism; the recipe owns the framework-specific how.
Process recipes are resolved by an orchestrator, keyed by (phase × framework), at a lifecycle moment — they are never matched by capability during free task work, where they would pollute context. That is why they publish to their own index, process-recipes.txt (separate from llms.txt and agentic-recipes.txt), and live under their own docs root, docs/process-recipes/.
Routing table
| Phase | Framework | Recipe | When to use |
|---|---|---|---|
research |
drupal |
Prior-art research | A Drupal project must establish prior art — first in its own custom code and exported configuration, then on drupal.org and in contrib by usage, maintenance, security coverage and core-version fit — before any custom build. |
design |
drupal |
Design | Turning researched requirements into a service-based architecture — business logic in injected services, a Drush entry point, and the form / entity / plugin pattern per component. |
test-authoring |
drupal |
Test authoring | Writing a Drupal test before the code exists — which tier the behaviour belongs at, where the file goes and what it is called, how the criterion it specifies is traced to it, and what a Drupal test may not do. Stops at red. |
implement |
drupal |
Coding standards and test discipline | Turning a test that already fails green, under Drupal coding standards and the implementation-time security rules, then refactoring under a green bar. |
test-execution |
drupal |
Test execution | Running a Drupal test at any scope — the suite, one file, one case, the tests covering a change, or the cheapest thing that proves the runner works — with the tier's cost and how to read what came back. |
review |
drupal |
Implementation review checks | Validating a Drupal implementation against its architecture and Drupal security — static \Drupal:: in new code, logic in forms/controllers, Form API CSRF — before acceptance. |
e2e-setup |
drupal |
ATK end-to-end test setup | A Drupal project (DDEV + Playwright) needs an end-to-end harness review can run over its surfaces. |
visual-regression |
drupal |
Visual-regression setup | A Drupal project needs a visual-regression suite that reads its surfaces from a file and names each baseline from them. |
worktree-environment |
drupal |
Worktree environment | A task built in its own git worktree on a DDEV project needs a running site of its own, seeded from the main checkout, with an address review can run against. |
research |
claude-code-plugins |
Plugin prior-art research | A Claude Code plugin project must establish prior art (reuse / extend / build-new) before scaffolding a component. |
design |
claude-code-plugins |
Design | Turning a researched need into a component map — type choice, progressive disclosure, manifest + boundary. |
test-authoring |
claude-code-plugins |
Test authoring | Writing a spec for the executable code a plugin ships — a hook script, a validator, a helper — before that code exists, and knowing why a component that is only instructions is paper-traced instead. |
implement |
claude-code-plugins |
Component authoring standards | Authoring skills / commands / agents / hooks to contract, with paper-test as the test-first gate for the part that is prose, and a spec that already fails for the part that is executable code. |
test-execution |
claude-code-plugins |
Test execution | Establishing that this framework has no test harness, what the paper-test and the structural validator answer instead, and which framework answers for a component that ships executable code. |
review |
claude-code-plugins |
Plugin review checks | Validating a plugin (structural + semantic) before acceptance, routing each check to its owning tool. |
research |
php-cli |
PHP CLI prior-art research | A PHP CLI project (a Composer library/app whose interface is one or more binaries) must establish prior art (reuse / extend / build-new) before building — Packagist is dense. |
design |
php-cli |
Design | Turning a researched need into a library-first architecture — the library/CLI boundary, the entrypoint contract (exit codes, stream split, machine-readable output), and the dependency posture. |
test-authoring |
php-cli |
Test authoring | Writing a PHP CLI test before the code exists — which level the behaviour belongs at, where the file goes, what the class and method are called, how the criterion is traced to it, and what a PHPUnit test may not do. Stops at red. |
implement |
php-cli |
PHP CLI standards and tests | Turning a test that already fails green, under PSR-12 / strict_types, with the flag-and-exit-code coverage check and the extensionless-binary syntax check. |
test-execution |
php-cli |
Test execution | Running a PHP CLI project's tests at any scope, with the PHPUnit filter and exit-code traps that make a run mean something other than what it reads as. |
review |
php-cli |
PHP CLI review checks | Validating a PHP CLI implementation (boundary, exit-code contract, dependency policy, security sinks, every binary linted) before acceptance. |
research |
go |
Go prior-art research | A Go project must decide whether a capability needs a dependency at all — standard library first, then transitive depth, deprecation and reachable-vulnerability evidence per candidate. |
design |
go |
Design | Turning researched requirements into a package layout whose boundary the compiler enforces — internal/ by default, cmd/ as a shim, the module path and its compatibility promise, consumer-side interfaces, context and the exported error surface. |
test-authoring |
go |
Test authoring | Writing a Go test before the code exists — which level the behaviour belongs at, which package the file declares, what the test function is called, how the criterion is traced to it, and what a Go test may not do. Stops at red. |
implement |
go |
Go standards and tests | Turning a test that already fails green, under gofmt and the vet floor, with -race in the ordinary test command and the map-iteration and unstable-sort ordering traps. |
test-execution |
go |
Test execution | Running a Go module's tests at any scope, with the -run over-selection trap, the silent zero-exit cases, and the marker that separates a failed assertion from a package that never compiled. |
review |
go |
Go review checks | Validating a Go change against its architecture and the toolchain gates — including the three gates commonly written in a form that can never fail — before acceptance. |
research |
python-cli |
Python prior-art research | A Python project must establish what already exists before anything is written — the standard library first, then PyPI, with each candidate judged on maintenance, typing, dependency weight, licence, yanked status and a pre-adoption vulnerability audit rather than on stars. |
design |
python-cli |
Design | Turning researched requirements into a library-first architecture with a thin console script — the package boundary, a named programmatic entry point per capability, the entrypoint contract (exit codes, stream split, machine-readable output), protocol seams, and the dependency and typing postures. |
test-authoring |
python-cli |
Test authoring | Writing a Python test before the code exists — which level the behaviour belongs at, where the file goes, what the test function is called, how the criterion is traced to it, and what a pytest test may not do. Stops at red. |
implement |
python-cli |
Python implementation standards and tests | Turning a test that already fails green, under the project's formatting, linting and typing standards — one exception type per failure class, and the import-time, mutable-default and shell-injection traps. |
test-execution |
python-cli |
Test execution | Running a Python project's tests at any scope through the runner the project declares, with the node-identifier form and the exit codes that separate a failed assertion from a collection that never happened. |
review |
python-cli |
Python review checks | Validating a Python change against its architecture and the toolchain gates in a blocking order, plus the conformance reads a linter structurally cannot make, before acceptance. |
claude-code-pluginsbinds six phases. A Claude Code plugin has no rendered or behavioural runtime surface, so this framework declares noe2e-setuporvisual-regressionrecipe. Do not run/setup-e2eor/setup-visual-regressionon a plugin project — the loader correctly returns no recipe, but those commands' generic fallback would still try to scaffold a Playwright harness that does not apply.
php-clibinds six phases. A PHP CLI tool has no rendered or behavioural runtime surface, so this framework declares noe2e-setuporvisual-regressionrecipe. Do not run/setup-e2eor/setup-visual-regressionon a PHP CLI project — the loader correctly returns no recipe, but those commands' generic fallback would still try to scaffold a Playwright harness that does not apply. "No e2e" means no browser e2e: a CLI tool's end-to-end shape — run the built binary against a fixture tree and assert on output and exit code — is a test level chosen intest-authoringand checked atreview, not ane2e-setupbinding.
gobinds six phases, and declares no change-impact globs. A Go module has no rendered or browser surface, so this framework declares noe2e-setuporvisual-regressionrecipe — and, because change-impact globs exist to route a changed file to exactly those two gates, the review recipe deliberately declares none. A Go CLI's end-to-end shape is a test level, not a phase: thetest-authoringrecipe names it as the entry-point level (callingrun(ctx, args, stdin, stdout, stderr) errorwith buffers) and the subprocess level (for the exit status, signals, and a real stdin pipe), and it is checked under the test gate atreview. Thereviewrecipe does declare## Code-quality extensions, and that one is load-bearing rather than optional: no Go extension is in the framework-neutral change-scoping floor, so without it a pure-Go change filters to an empty file list and every change-scoped gate skips itself — a clean-looking run that examined nothing.
python-clibinds six phases, and declares no change-impact globs. A Python library or console-script tool has no rendered or browser surface, so this framework declares noe2e-setuporvisual-regressionrecipe — and, because change-impact globs exist to route a changed file to exactly those two gates, the review recipe deliberately declares none. A CLI's end-to-end shape is a test level, not a phase: thetest-authoringrecipe names it as the entry-point level the tests import, and it is checked under the test gate atreview. Thereviewrecipe does declare## Code-quality extensions, and that one is load-bearing rather than optional: no Python extension is in the framework-neutral change-scoping floor, so without it a pure-Python change filters to an empty file list and every change-scoped gate skips itself. It declares.py,.pyi(a stub is the declared public typing surface, not dead text) and.toml— the last one deliberately, because a change that touches onlypyproject.tomlcarries the dependency and version decisions this framework blocks on. The machine-checkable## Preconditionsfor running a test — the manifest, the interpreter and the runner in the project's own environment — are declared by thetest-executionrecipe, alongside the commands they are conditions of.
What each phase type is for
The authoring rules below specify a recipe's form. This section specifies its job — what a recipe of each type must decide, and what belongs to a different type. Read it before authoring a set for a new framework, because the form is identical across every one of them and the form alone will not stop you putting review's content in implement, or putting something in a recipe that should not be in one.
The invariant that governs every type. The plugin owns the mechanism and the gate; the recipe owns only what is genuinely specific to its stack, and references canonical sources rather than restating them. A recipe ships no code assets. Two tests before anything goes in a recipe:
- Would this sentence be identical for another stack? Then it is not framework knowledge, and it belongs in a guide the recipe cites — not copied into each framework's recipe. A rule restated once per stack has no single place to correct it, and nothing detects the copies diverging.
- Does this run something, or record a result? Then it is the plugin's. The recipe supplies the method the gate evaluates; it does not own the gate.
| Type | Its job | Not its job |
|---|---|---|
research |
Establish what already exists, in the project's own code and configuration first and then in the ecosystem, and return named candidates with the evidence behind each, ordered by closeness. | Returning a verdict. The reuse-or-build call belongs to the stage that decides the architecture, and a recipe that returns one is one edit away from a recipe that decides. Judging code that does not exist yet. |
design |
Turn researched requirements into structure — where business logic lives, the programmatic entry point, which of the stack's patterns each component takes, and the boundary the language or framework enforces. Return the units to build and the order to build them in; the caller records them, and there is no architecture document. | Coding standards. Test levels. Anything about how the code will be written. |
implement |
The rules applied while code is written: coding standards, the implementation-time security guarantees, and the green-and-refactor half of the cycle applied to a test that arrived red. This is where a stack's best practices live. | Running linters — the recipe judges what a standard means, the tooling runs it. Post-hoc validation. Choosing a test's level or writing it — that is test-authoring's, because this reader may write production code and that one may not. |
test-authoring |
Where a test goes, what shape it takes, and how it is traced to the criterion it specifies. Read by the context that writes tests and cannot read production source. | Running the test. Writing the code that passes it. |
test-execution |
The command that runs a test at each scope a caller asks for, its cost, and how to read what came back. Its several readers are the reason it is its own file: the failing-test step, the baseline, each build check, and the fixer all ask the same question at different moments. | Deciding what to test, or judging the result. It returns a command and the means to read the output; it runs nothing. |
protected-tests |
What may change a test: which classes of test may be weakened at all, what an exemption looks like, and who may grant one. Read by a rule rather than by a model. | Judging a test's content. Deciding whether a change is warranted. Saying which files are tests — that is declared under implement, in ## Oracle files, and stays there. |
build-checks |
The checks a script runs after code is written and before anything judges it, in the stack's own terms. | The blocking acceptance validations — those are review's, and the difference is when they run, not what they run. |
review |
The blocking validations run before work is accepted, in the stack's own terms, in a deliberate order. | Restating the generic review. Re-authoring checks implement already applied inline. |
e2e-setup |
One-time setup of an end-to-end harness: the commands that install it, the files it writes, and the sources discovery reads to propose the surfaces. | Running the suite. Deciding which surfaces a project has: discovery proposes, a person confirms. |
visual-regression |
One-time setup of a visual-regression suite: the commands that install it, the files it writes, the viewports it starts from, and the sources discovery reads. | Capturing or approving baselines on an ongoing basis. Holding an address. |
worktree-environment |
Giving a task's git worktree a running environment of its own: what must be true first, the commands that bring it up and seed it from the main checkout, the one command that prints its address, the commands that tear it down, and which tasks build on the served checkout instead. | Bringing an environment up without a person's yes. Holding a database command of its own: the consumer runs the recipe's lines. A framework with no served environment declares nothing at this point, and a worktree there has files and no site. |
Two of these phases are named and not yet written. protected-tests and build-checks have
their names and their jobs above, and no recipe files. They were named alongside test-authoring
and ahead of their content deliberately: a name becomes a contract the moment anything resolves
against it, and picking them one at a time from inside a single step produces a set that does not
fit together. Until each one's content lands, the material stays where it is.
## Oracle files stays under implement, and is not split. The open question was whether the
section should move to whichever phase writes tests, since it answers "which files here are tests".
The answer is no. It is not a list of tests: it is a list of things a builder must not quietly
weaken, and test material is a minority of its rows in every framework — the rest are the
static-analysis baseline, the linter configuration, the coverage threshold and the dependency
manifest, and no two frameworks carry the same set. Moving it would take three or four rows away
from their reader to relocate one or two, and splitting it would state a glob in two files with
nothing to detect them diverging. What test-authoring needs from it is the file pattern, which it
cites rather than repeats.
Not every framework binds every type. A stack with no rendered or browser surface declares no
e2e-setup and no visual-regression recipe, and that is a complete set, not a gap — see the
per-framework notes above the authoring rules. A CLI's end-to-end shape is a test level chosen
in test-authoring, checked under review, never a phase of its own.
test-authoring and implement share the TDD loop between them; e2e-setup and
visual-regression are outside it. The tests a test-authoring recipe chooses a level for are
written before the code and seen to fail, where the red comes from a behaviour that does not exist
yet rather than from code broken to force it — they constrain a design that does not exist yet. That
recipe stops there; implement takes the red test and turns it green without changing it. The
suites the other two phases wire up run against something already built, cannot drive a design
decision, and are therefore outer verification. The line is not whether a browser or a subprocess is
involved: Drupal's FunctionalJavascript tier drives a real browser and is inside the loop, and a
CLI's fixture-driven end-to-end level spawns a process and is inside it too. What follows is that
e2e or visual-regression coverage never substitutes for a level test-authoring must choose, and is
reported separately rather than counted toward the test-first requirement. The stack-neutral
statement of this, and of what makes an added test excess rather than coverage, lives in
development/tdd-spec-driven and is
cited by each recipe rather than restated in it.
The split is a reader constraint, not a filing preference. The context that writes tests cannot
read production source and cannot write production code; the context that implements can write
production code and must not rewrite a test. Every implement recipe ends in a numbered sequence
whose later steps instruct the reader to write or change production code, which is why the level
rule, the file placement and the test-naming rules moved into their own file rather than being cited
in place: a reader that may not write production code cannot be handed a file that tells it to.
What each type may declare
Five types carry machine-readable declarations. research, design and test-authoring carry
none — they are prose method, consumed by an agent, not parsed by a script. test-authoring was
written that way deliberately: it chooses a level, a path and a name, and nothing about that is a
list a script reads back. What the two unwritten phases declare is decided with their content, not
here.
| Type | Declaration (exact heading) | Posture |
|---|---|---|
implement |
## Oracle files, ## Routing hints, ## Preconditions, ## Configuration gate, ## Unit declaration |
fail-open (## Preconditions fails closed; ## Configuration gate fails closed for an order whose proof is the gate; a missing ## Unit declaration means no file declares a unit, so a red that holds only a harness marker is refused for every order) |
test-execution |
## Test commands, ## Preconditions |
fail-closed (both) |
review |
## Change-impact globs, ## Code-quality extensions, ## Check commands, ## Surface commands |
fail-open (## Check commands and ## Surface commands fail closed) |
visual-regression |
## Install, ## Files, ## Viewports, ## Surfaces, ## Discovery |
fail-closed (## Install with no sh block refuses the install; the rest read as empty) |
e2e-setup |
## Install, ## Files, ## Surfaces, ## Discovery |
fail-closed (## Install with no sh block refuses the install; the rest read as empty) |
worktree-environment |
## Preconditions (prose and one sh line), ## Tokens, ## Files, ## Bring up (twice, around ## Address), ## Address, ## Tear down, ## Build in place |
fail-closed (any of ## Bring up, ## Address or ## Tear down with no sh block refuses the offer, because an environment nobody can remove is not offered; a ## Preconditions line that exits non-zero refuses it, removes the files that run wrote and commits nothing; a ## Tokens command that prints nothing refuses it by the token's name; ## Files follows the setup rule; ## Build in place reads as empty) |
Spelling is load-bearing. A fail-open declaration with a misspelled heading does not error — it silently degrades to the neutral floor, and the run looks clean while checking less than you think.
## Test commands is parsed, and it fails closed. A test-execution recipe declares six rows —
suite, file, test, changed, smoke, mutation — and each is either a command or a named
statement that this framework has none. Absent is an answer; a row nobody wrote is not, which is why the set is
fixed and scripts/validate_recipes.py rejects a recipe missing one. A command is a list of argv
tokens, never a shell string, and a token that is exactly a {placeholder} is substituted whole —
the same rule check: lives under, for the same reason. Each command carries a cost: of
every-attempt or end-of-task, because a caller without one either runs the cheapest thing and
under-checks or runs everything on every attempt. Alongside the rows, failure_signal: says how to
tell a failed assertion from a harness that never reached the behaviour, in that harness's own
output — the frameworks differ sharply here, and two of them report a selector that matched nothing
as a success. harness: is the setup-gap signal, and there is no separate key for it: a red run whose
output holds a harness: marker and no assertion: marker never reached the behaviour, which is what
test-execution and test-authoring both call a setup gap, and a consumer refuses such a run as one
instead of freezing it as a red. It reads assertion: first, because PHPUnit numbers an erroring test
the same way as a failing one and a failure_line: selector matches both.
failure_line: on the suite row selects the lines a red baseline is subtracted on. A build
that starts from a commit whose suite is already red must subtract the old failures from the new
run, or every order spends its attempts on a question the suite cannot answer. Subtracting the
whole output line by line is exact for a linter and wrong for a test runner: the progress line
and the counts line change whenever a test is added, so the suite reads unmet on any red baseline.
The key is a regular expression in the same YAML shape as silent_pass:. It matches the lines
that name one failing test, one line per failure, and the recipe's prose says what else it can
catch. The four recipes with a suite command carry one, each observed on its runner:
'^[0-9]+\) [\w\\]+::\w+' for PHPUnit, '^(FAILED|ERROR) ' for pytest, '^\s*--- FAIL: ' for Go.
scripts/validate_recipes.py refuses a selector that does not compile or sits on an absent: row. A consumer subtracts only the matching lines, on
both sides, and records the selector it used; a recipe without the key keeps whole-output
subtraction, with the limit above.
The mutation row is a report, not a gate. Every mutation tool run for it — Infection, mutmut,
gremlins — exits 0 with surviving mutants, and gremlins exited 0 with its own efficacy threshold
unmet, so a caller reading exit status learns nothing from this row. Its trap: says where the
score and the survivors print, in that tool's own output, and that is what a caller reads. Two of
the three tools take no file list: mutmut scopes by pyproject.toml and gremlins by the module
root, so {paths} appears only where the tool documents positional file arguments.
These blocks are plain YAML in the body, introduced by a line that is exactly the key. A fenced ```yaml block reads the same to the parser and renders as a code block rather than as one collapsed paragraph, so fence anything longer than a few lines.
The preconditions: entries are parsed too, as of this change. The block has looked structured
since it was written and, until now, nothing read it, so a misspelled check:, owner: or id:
degraded in silence. The validator checks the entry keys and rejects an unknown one. Where nothing
owns an entry, say so — owner: operator for a toolchain the machine's owner installs — so that an
entry nobody owns and an entry whose owner was forgotten stop looking the same.
A check may assert on what it printed, with expect:. A check: is decided by its exit status,
and some commands answer in their output instead: ddev describe -j reports whether a project is
running and exits 0 either way. Where that is the case, the entry adds an optional expect: beside
its check:.
preconditions:
- id: test-runner
what: a running DDEV environment, so the runner the project documents can be reached at all
check: ddev describe -j
expect: '"status_desc":"running"'
owner: code-quality-tools:setup
The exit status is still read first and keeps every meaning it has: a command that is not found says
nothing about the condition, and any other non-zero exit is a condition that answered no. Only on a
zero exit does expect: decide, and it decides one way — the literal string appears in what the
command wrote to standard output, or the condition answered no. It is a substring test and nothing
more: no regular expression, no glob, no path into a document, no shell, for the same reason the
command itself never reaches one. Choose the string so that its presence is the answer, because
a JSON document reporting several things at once usually contains the string you meant to rule out —
and test it against every state the command can report, not just the two obvious ones. The example
above uses status_desc rather than status for exactly that reason: status appears once per
service, so a paused project still prints "status":"running" for whatever stayed up. An entry with
no expect: is decided exactly as before, so nothing that already works changes.
## Check commands is parsed, and it fails closed. A review recipe declares five rows —
coding-standards, static-analysis, security, duplication, design-metrics — and each is
either a command or a named statement that this framework has none. The first three are the checks
a build record runs before anything judges the code; the last two feed the review's own duplication
and design reads, and are absent on every framework whose recipe names no tool for them. Absent is an answer; a row nobody wrote is not, which is
why the set is fixed and scripts/validate_recipes.py rejects a recipe missing one. A command is
a list of argv tokens, never a shell string, and {paths} expands to one token per file in the
caller's file list, relative to the project root, never concatenated and never passed through a
shell — the same rule test_commands places on {file}, {test_id} and {paths} there. A row
answered absent: carries no argv: and no {paths}; its reason text is what a person reads when
they ask why the check never ran for this framework. A row with no {paths} runs whole, over
whatever scope its own tool takes. A tool that takes directories rather than files takes {dirs}:
one token per distinct directory that directly contains a file in the caller's list, relative to
the project root, with any directory that lies inside another listed one dropped, so that no file
is scanned twice.
Two optional keys exist for tools that do not fit the plain shape. extensions: lists the file
extensions the tool reads; where it is present, {paths} expands to only those files, because a
tool handed a file type it does not read either skips it in silence or parses it as its own
language and fails on it. Where the expansion is empty the row does not run and is recorded as not
applicable, never as met. No shipped row carries it today: the tool that first needed it, mypy,
turned out to replace the project's declared scope with whatever paths it is given, and now runs
whole instead. The key stays defined for the next tool that reads a file list and chokes on a type
in it. signal: empty-stdout marks a tool that cannot fail by
exit status: the caller reads a zero exit with anything on standard output as unmet, and a caller
that does not read the key must record the row as not run rather than met, because reading that
tool's exit status alone is a gate wired to nothing. Without the key, the exit status decides —
zero is met, and any other status is unmet. The list of ids grows by editing the validator and
this paragraph together, never by a recipe adding a row on its own: a consumer addresses a row by
an id it already knows, and an id it does not know is a row it never runs.
## Surface commands is parsed, and it fails closed. A review recipe declares six rows —
e2e-preflight, e2e, visual-regression, visual-regression-accept, visual-parity,
visual-parity-accept — the readiness check run before the e2e suite, the suites review runs over
a framework's user-visible surfaces, and the command that accepts a suite's current output as its
new baseline. An accept row is a second id, never a key on the suite's row. The rows take the same
keys as ## Check commands, plus silent_pass: on a row that runs a suite: how a run that
selected nothing prints itself, in that harness's own output, the same job the key does under
failure_signal:. Every code span in either text is a marker: a consumer reads a run whose
output holds one as having selected nothing, before it reads the exit status, so a span holds
only what the harness prints and every other name stays in plain words. The same rule reads every key
under failure_signal:, assertion: and harness: included, because a folded text offers a consumer no
other way to lift a marker out of it: a span there is text the runner prints whole, so a placeholder such
as a test's name or a package path has no place inside one, and an option name stays in plain words. A text that begins
None declares no marker. A framework with no such surfaces declares every row absent, each with its own
reason; the block is present either way, because rows that are not absent are how review knows a
framework has surfaces, and a missing block is a heading it could not find. One framework in this
catalog has surfaces, and its rows run on the host rather than in its container, because that is
where its e2e recipe put the browser.
Three things a consumer does with these rows. The e2e-preflight row runs before the e2e row;
a non-zero exit records the preflight unmet, reads the e2e check unknown, and the suite does not
run, because a site that is not ready and a journey that failed are two different findings. Each
suite row names its kind's own config with --config, so a caller never has to know which
directory a suite reads from. The two suite rows and the accept row carry {surfaces}, one
token: the caller fills it with the ids to run, or whose baselines are to be rewritten, joined by
|, and the harness matches it against test titles, which is why every title in a surface suite
begins with its surface id. A caller running the whole set fills it with every enabled id, and one
running the surfaces a change touched fills it with those, so a row with the token never runs with
an empty grep. The address a
suite runs against is never in a recipe or a file: the caller exports it as PLAYWRIGHT_BASE_URL
for the row, and every config the setup recipes write reads that variable.
## Install, ## Files, ## Viewports, ## Surfaces and ## Discovery are parsed from a setup
recipe. An e2e-setup or visual-regression recipe carries them beside its nine required
sections. ## Install holds fenced sh blocks, one command per line; a consumer runs each line as
arguments, never through a shell, and refuses a line carrying a shell character. ## Files holds
one fenced block per file, the path relative to the code tree as the second word of the fence; a
consumer writes each file only where it is absent, and refuses the whole install when a file exists
with different content. ## Viewports (visual only) and ## Surfaces each hold one json block,
the viewport list a surface file starts with and the seed rows discovery proposes. ## Discovery
is prose a consumer prints for the person who confirms the list. The commands review runs over the
installed harness are not here: they are the ## Surface commands rows of the same framework's
review recipe, and a setup recipe names those ids in one sentence so a reader knows where to look.
## Preconditions, ## Tokens, ## Files, ## Bring up, ## Address, ## Tear down and
## Build in place are parsed from a worktree-environment recipe, in document order.
## Preconditions keeps its prose, the reason for each check, and holds one fenced sh block, one
command per line, {codePath} as a whole argument. The consumer runs it in the worktree after
## Files is written and before anything is committed or a token runs; every line must exit 0,
and a line that does not refuses the bring-up, prints the command's output, removes the files that
run wrote, and commits nothing. ## Tokens holds one fenced sh
block per token, the token's name as the fence's second word, one command. The consumer runs each
in the worktree after ## Files is written and before the first ## Bring up, as arguments and
never through a shell, and the first line of standard output is the token's value. {codePath}, the main checkout's path, is the one token
the consumer fills itself, and it may appear in those commands as a whole argument. A command that
prints nothing or exits non-zero refuses the bring-up and names the token. A command that needs a
pipe or a filter lives in a script the recipe writes under ## Files, by the setup recipes' rule.
The consumer commits what ## Files writes on the task's branch before it runs a token command,
because an untracked file fails the clean-tree check and git worktree remove refuses the tree.
## Bring up appears twice: the blocks before the ## Address heading run first, then the address
command, then the blocks after it. ## Address holds one command whose output is key: value
lines. address: is the site's address, recorded in the task and exported as
PLAYWRIGHT_BASE_URL for every run of that task. root: is the directory the environment tool
resolved the project from; the consumer stops when it is not the worktree, because a tool that
resolved another project would seed that one. Every other key is a token for the blocks that
follow and for ## Tear down. ## Build in place is prose the consumer shows at the point of
choice: which kinds of task should not have a worktree environment at all, and why.
A framework with no served environment writes no recipe at this
point, and a consumer that finds none says so once and goes on with a worktree that has files and
no site.
## Configuration gate is the contract an implement recipe offers for a unit that writes no
test, and it fails closed for the orders that need it. As of 2026-09-14 only
drupal/standards-and-tests.md 0.7.0 carries one, and no consumer reads it yet; this is the shape a
consumer meets. A unit whose deliverable is exported configuration has no PHPUnit test to freeze.
The design recipe sizes it around the operation it performs, and this section is its proof. The
block is one sh fence, one command per line, each line one command split on spaces and never run
through a shell, from the worktree's project root. Its first line restores the database to the seed
the worktree-environment recipe took at bring-up, so the import that follows is real. Every line
must exit 0; the first non-zero line is the finding, and the recipe's prose says what each line's
failure means. A consumer reads it the way it reads the environment recipe's ## Bring up, as lines,
not the way it reads ## Test commands, as argv: rows. An order the design marks as gate-proved
is frozen with zero tests and built with these lines as its check. In a framework whose implement
recipe has no such section, that order is refused at design, because a unit nobody can prove is not
cut. A framework that answers nothing with configuration declares no section, and nothing changes
for it.
## Unit declaration names the file whose presence makes a unit exist, as data. As of
2026-09-14 only drupal/standards-and-tests.md 0.8.0 carries one, and no consumer reads it yet;
this is the shape a consumer meets. A new module's first red is a setup gap by the harness: rule above: its
tests error where the harness enables the module, before any assertion runs, and nothing in a build
may write the module first to get a better red. The block is one unit_declaration: mapping in the
same YAML shape as preconditions:, holding globs:, the patterns of the file that declares a unit
in this framework. An order whose owned files match one is a new unit, and a consumer may accept a
red that holds only a harness marker for it, with the reason that nothing can fail an assertion
before the unit exists. scripts/validate_recipes.py checks the block when present: a mapping holding
globs: and no other key, a non-empty list of strings. The review recipe's ## Change-impact globs lists the same file
to say which checks a change to it triggers, which is a different question; a consumer wanting to
know what makes a unit exist reads this block. A framework with no such file declares no section,
and a harness-only red stays refused for it.
## Oracle files is parsed, not just read. As of 2026-09-01 a consumer takes the globs off the
row whose type is test_delete to answer "which files in this repository are tests", instead of
trusting a list the caller supplied. Three things are therefore load-bearing inside that section and
are enforced by scripts/validate_recipes.py: the first ``json fence under the H2 must parse as a
top-level array of flat objects; every row must carry exactlytype,globs,changes,oracle_class,severity; andtest_deletemust appear at most once, because it is the selector.
The markdown table above the fence and the fence itself state the same rules, and the validator now
checks that they agree — before it did not, so the table a person reads could drift away from the
rules a machine applies. When restructuring this section, keep atest_deleterow resolvable from
the recipe's own body: a row present only by inheritance is invisible to anything reading the
published page. Where a stack
has no extension in the framework-neutral change-scoping floor,## Code-quality extensions` stops being
optional: without it every change-scoped gate filters to an empty file list and skips itself.
Authoring a process recipe
A process recipe is authored to the same recipe_schema_version 1.0.0 standard as a task recipe — same validator, same required sections. It differs only in where it lives and three routing keys. scripts/validate_recipes.py enforces all of this (it scans both recipe roots).
1. Location is the class. Put the file at docs/process-recipes/<domain>/<name>.md (e.g. docs/process-recipes/drupal/e2e-setup-atk.md). The first path segment is the domain. Anything under this root is a process recipe; nothing else is. This is what keeps process recipes out of the task index — the task generator never scans here.
2. Routing-first frontmatter (first three keys, in order):
name: drupal_e2e_setup_atk # globally unique, snake_case
capability: e2e-setup # the PHASE — capability IS the lifecycle phase
description: Use when … # single-line when-to-use trigger
3. Required metadata + the process routing keys:
label: ATK end-to-end test setup (Drupal)
recipe_schema_version: 1.0.0
version: 0.1.0
recipe_class: process # required; self-declares the class
framework: drupal # required; the 2nd half of the resolution key
capabilitydoubles as the phase. There is no separateapplies_to_phase. (If you add one, it must equalcapability, or validation fails.)- Any other keys (
drupal_compatibility,requires_modules,assumes,authors,license, …) are free-form — the validator ignores them.
4. Required body sections (same nine as every recipe): Goal, Opinion, Preconditions, Input contract, Sequence, Data flow, State-awareness contract, Verifier, References.
5. Declare the tooling your method needs. A process recipe knows which tools its method requires; the caller does not. It knows it wants a standards check, not that a Drupal standards check means phpcs. Declare them by tool name, and the caller resolves each one against the tooling index rather than guessing what a framework's method runs:
requires_tooling: # optional; tool names, resolved for THIS recipe's framework
- phpcs
- phpstan
The name is the whole contract — a tooling recipe is named for its tool, and whatever needs the tool refers to it by that name. scripts/validate_recipes.py checks that each declared name resolves to a real tooling recipe for the recipe's own framework, so a name that resolves to nothing fails when the recipe is published rather than when somebody runs it. The key is optional and checked only when present, so a recipe whose framework has no tooling recipes yet stays valid.
6. Reference origin; do not ship code assets. A process recipe carries the framework-specific binding as prose and references canonical sources (module docs, Playwright, etc.) — it does not bake in .ts/.sh files. The plugin owns the generic machinery; the recipe binds the framework into it.
What the build produces
On deploy, scripts/generate_process_recipes.py emits one routing line per recipe into process-recipes.txt, plus process-recipes.hash:
- <name> [phase=<phase> framework=<framework>] (sha:XXXXXXXX): <when-to-use> — <site-url>
The orchestrator matches on (phase, framework) without fetching the body; the per-recipe (sha:XXXXXXXX) gates the body cache; the body is fetched as raw markdown from the site-url.