Skip to content

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-plugins binds six phases. A Claude Code plugin has no rendered or behavioural runtime surface, so this framework declares no e2e-setup or visual-regression recipe. Do not run /setup-e2e or /setup-visual-regression on 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-cli binds six phases. A PHP CLI tool has no rendered or behavioural runtime surface, so this framework declares no e2e-setup or visual-regression recipe. Do not run /setup-e2e or /setup-visual-regression on 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 in test-authoring and checked at review, not an e2e-setup binding.

go binds six phases, and declares no change-impact globs. A Go module has no rendered or browser surface, so this framework declares no e2e-setup or visual-regression recipe — 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: the test-authoring recipe names it as the entry-point level (calling run(ctx, args, stdin, stdout, stderr) error with buffers) and the subprocess level (for the exit status, signals, and a real stdin pipe), and it is checked under the test gate at review. The review recipe 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-cli binds 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 no e2e-setup or visual-regression recipe — 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: the test-authoring recipe names it as the entry-point level the tests import, and it is checked under the test gate at review. The review recipe 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 only pyproject.toml carries the dependency and version decisions this framework blocks on. The machine-checkable ## Preconditions for running a test — the manifest, the interpreter and the runner in the project's own environment — are declared by the test-execution recipe, 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
  • capability doubles as the phase. There is no separate applies_to_phase. (If you add one, it must equal capability, 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.