Four RFCs describe layers of the same machine, from the outside in.
RFC-026, Werkvoorraad. Which articles must be enriched and in what order. It produces tasks.
RFC-027, Enrichment Quality. The roles, the gates and the failure classes that decide whether a single article is enriched well. It consumes tasks.
RFC-028, Steps and Runtimes. How a single step in that chain runs.
RFC-029, Test Fixtures. The yardstick for the whole.
This RFC is the third layer.
RFC-027 splits enrichment into roles with separate contexts: a qualifier that reads and classifies, a producer that translates within that classification, an adversary that attacks the translation, and an arbiter that decides. Those roles are separate agent runs, the firewalls between them are the reason for the separation, and the pipeline has no way to say what a run is.
The seam it has instead couples the agent to the domain. The invocation trait takes the enrichment payload, so the thing that knows how to run an agent also knows what enrichment means; a second chain therefore needs a second trait, which is why document conversion and law structuring each grew their own. Below that seam the tool allowlist of one CLI travels through three signatures as a boolean named after a shell.
The cost of having no step object is recorded in issue #1036. In that failure an instruction prescribes work the runtime cannot perform, nothing compares the two, and the step runs to a clean exit having done nothing. A gate that only inspects the final file cannot tell that apart from a step that had nothing to do.
This RFC defines the layer underneath RFC-027’s roles: a step is data that says what must happen and what it needs, a runtime is a thing that can execute such a step and translate its needs into its own flags, and the choice of runtime, model and budget per step is configuration. It also decides how far that abstraction reaches, because half the design is about what it refuses to hide.
The boundary is drawn where an ORM draws one. An ORM hides which database answers a query and does not hide that a query can fail, that a column has a type, or that a join costs something. The same applies here: the runtime layer hides which agent runs a step, and refuses to hide that a step needs a shell, that a model has a context window, or that an instruction assumes a tool loop.
A step must declare what it needs, and the declaration must be checkable against the instruction. A capability field a human fills in by hand next to a prompt a human writes by hand reproduces the failure it is meant to prevent: the same person forgets in both places, and comparing two hand-written declarations catches nothing. The declaration is only worth its cost when part of it is derived and part of it is verified against what the run did.
A step that cannot be executed as specified must not start. A missing capability, a model whose context window cannot hold the mounted inputs, an instruction that assumes a tool loop on a runtime that has none: all three are known before a process is spawned, and all three are configuration faults rather than properties of the law being translated.
A step must not be able to see what the chain withheld from it. RFC-027 requires the adversary to re-derive the classification without the reading note, and the qualifier to classify without knowing what the model looks like. That must be a property of what is placed in front of the agent rather than a sentence in a prompt, and the enforcement level must be stated, because a context boundary between cooperating processes is not a security boundary.
The runtime must be identified in the provenance well enough to answer “which agent wrote this”. Runtime name, CLI version, resolved model, the instruction digest, the digest of every mounted skill, the granted capabilities and what the run did with them.
Per-step configuration must reach model choice and budget. The reading is the expensive judgement and the production is bound by it; a single model for the whole chain overpays for one or underpays for the other.
Failure must be typed at the point it is known. Whether a job retries, exhausts a law, or waits for a human is decided from the shape of the failure, and reconstructing that shape by matching substrings against an error’s display format makes a formatting change a behaviour change.
Every agent run must be countable. The spend guard, the throughput budget and the question “which laws ran under the old qualifier” are all queries over runs. A chain multiplies runs per job, so a counter that counts jobs stops meaning what it meant.
A step is data. It states what must happen and what the result must satisfy, and it says nothing about which agent, which model or which flags. The module is packages/pipeline/src/agent/.
timeout, model, attempts and runtime are deliberately absent. StepSpec says what the step wants; StepLimits says what it is allowed to spend and on what. A step must stay readable without knowing which agent runs it, and the same step must mean the same thing in a local single-law run and in the worker.
One list with a mode, because an artefact appearing in two lists can be given two contradictory modes. Modify covers the law YAML, which is input and output at once. CreateSet covers the case where the number and the names of the outputs are the agent’s choice, as they are for MvT-derived feature files; without it those files are silently discarded by the write-back rule.
Mount paths pass the same normaliser that guards payload paths against traversal and absolute paths. Mounts originate in Rust code, which makes that theoretical until the first one arrives from configuration, and by then the guard has to already be there.
The type is named Judgement rather than Verdict because the source gate already owns Verdict in the same crate.
The signature takes the bus and two snapshots rather than the working directory. A criterion that could only see the directory the agent worked in could express none of the checks that matter: article count unchanged is a delta, window progress is a delta against a window the planner assigned, binding integrity reads sibling laws from the corpus root, and the schema gate only applies when the run changed something.
The criteria the chain needs, with their existing sources:
| Criterion | Source |
|---|---|
SchemaValid | enrich_v2::checks::schema_errors |
ArticleCountUnchanged | inline in the enrichment orchestration |
WindowProgress | inline, including the rule that a chunk report must name an article of this window |
NoNewFindings { max } | enrich_v2::checks::run |
SourceGateVerified | enrich_v2::source_gate::GateReport::passes |
OutputsPresent | the empty-output guards in law and document conversion |
NoUndeclaredToolDenials | new, from the tool ledger |
DeclaredCapabilitiesWereUsed | new, from the tool ledger |
AllOf(Vec<Arc<dyn Criterion>>) combines them and reports the heaviest FailureKind among the unmet ones. The producer step gets AllOf([ArticleCountUnchanged, SchemaValid, WindowProgress]).
SourceGateVerified cannot be a criterion on its own, because verification compares the corpus file against the official toestand and that comparison needs the official text. The gate is a native step that fetches and publishes the official articles as an artefact; the criterion compares two published artefacts. That split is the general rule: a criterion reads the bus and never the network.
OutputsPresent needs the distinction the chunk guard already draws. A step that ran and legitimately produced nothing is not a failed step: RFC-027 lets the adversary state that no tenable counter-reading exists on a lens, and that is an empty findings file from a successful run. Emptiness therefore only fails where the step’s own contract says the artefact must be non-empty, and where a step may legitimately be empty it must publish a report saying so.
Mark always writes a record. There is no variant that continues silently, because a step that produced nothing and reported nothing is the failure this design removes.
Repair generalises the two identical one-round repair loops that exist separately for schema errors in enrichment and validation errors in law conversion: same policy, two implementations, two prompt builders.
repair_rounds is a ceiling per step rather than a fixed count, resolved through the same three profile layers as model and timeout, so the schema step and the marking step can be given different budgets. The open question was never whether a second round helps but at which step it helps: a schema error is repaired or is not, while a coverage question can genuinely take a second look, and one number over the whole chain averages those two into an answer to neither.
A ceiling on its own buys wall clock, since a second round over an artefact the first one did not touch costs an agent run and comes back with the same reasons. Three conditions therefore end the rounds early, and each says that the next round has no evidence behind it.
Cleared. The step’s criterion is met and there is nothing left to answer.
Unchanged. The agent left the artefact byte-identical. Nothing it did can have removed a reason, so nothing suggests the next round differs.
No decrease. The artefact changed and the number of reasons did not fall. That is churn or a trade, and neither predicts an improvement.
Per round the chain records the reasons before and after, the marking count before and after, whether the artefact changed, and which of the four stops ended the sequence. The marking count travels beside the reason count because a falling reason count bought by declaring more of the law unmodellable is the opposite outcome from one bought by translating it, and a measurement that reports only the reasons scores the two the same. RFC-027 makes the same point about counters beside gates.
The default stays one round per step. RFC-027 gives the argument: a disagreement that survives the first round is usually either something the text leaves open, which the marking channels take, or a defect in the instruction, which no retry inside one law repairs. Raising the number is a measurement rather than a fix, and the per-round records are what that measurement reads.
A capability is what the step must be able to do, stated in the vocabulary of the work. Tool allowlists are a translation of it, and the translation belongs to the runtime.
Corpus access is not a capability. Reading another law is fulfilled by mounting that law, which the planner resolves to concrete paths, so it is a mount and nothing else. A whole-corpus capability is deliberately absent: the sparse checkout exists because indexing the whole corpus overran the context window and the memory ceiling, and a capability that reinstates it is one that would have to be withdrawn later.
FetchSources on an agent step is almost always wrong; the worker should fetch and mount the result. The source gate already works that way: the fetching belongs to harvesting, the worker validates what it fetched, and the agent reads what the worker vouched for. The capability exists anyway, because the alternative is the failure it is named after: an instruction that requires the network, a runtime that does not provide it, and nothing that says so.
The same holds for RunTools. A validate-and-test loop inside the agent is not a capability to be granted; it disappears, because the validator is a library call in the worker and its result is a success criterion. RunTools remains for work that is genuinely the agent’s, such as converting a document format the deterministic extractors do not handle.
Three things derive the required set, and only the third is written by hand.
A mount of mode Create, Modify or CreateSet yields WriteFiles; Modify also yields EditInPlace; any input yields ReadFiles. A step that mounts an output and forgets to ask for write access therefore cannot exist.
The skill derivation makes the declaration self-checking. Without it the declaration is a second place to remember the same thing. A step that mounts a research skill whose front matter declares web access acquires FetchSources and SearchWeb from that mount, and a runtime that cannot provide them refuses the step before it starts. The instruction and the grant stop being two independent guesses, because the skill is the instruction’s own statement of what it needs.
Checking a declaration against a declaration closes one failure mode and leaves the other open: it catches a step asking for something the runtime does not know, and it cannot catch a step whose instruction requires something nobody declared. Three levels are needed, and only the third settles what happened.
Declared. AgentRuntime::capabilities(&limits), hand-written per runtime and model. Proves nothing on its own.
Realised. CommandBuilder::probe() at construction, once per process, checking that the named tools are on PATH and that the CLI’s flag vocabulary supports the translation this builder emits. A version bump that removes a flag is otherwise a silent change to the effective grant, which is the failure of issue #1036 in another form.
Observed. The agent’s own event stream, folded into a bounded ledger while the process runs.
Two facts become hard failures instead of silence. A denied call to a capability the step did not declare is a defect in the step, not in the law, and fails as Misconfigured. A declared capability that was never exercised means the egress is closed, or the account does not have the tool, or the instruction never asks for it, and all three are worth knowing.
This is the only mechanism in the design that would have caught the failure in #1036 on the run that had it. Without the ledger and the skill derivation, a capability field is a comment with a type.
A shortfall is knowable before a process exists, so it is refused there.
Three refusals, all before spawning. A capability the runtime does not provide. A modality the runtime does not support, where an Agentic instruction meets a single-turn runtime. And a step whose mounts do not fit, where the planned mount bytes exceed what the resolved model’s context window can hold.
The third addresses the difference that separates the two existing lanes. A model with a 32k context window does not fail the producer step because it holds too many tools; it fails because the law does not fit. The mounted bytes are known and the window is known, so “this model cannot do this step” is a refusal rather than an invented output format discovered later by a schema check.
There is no fallback path. A step that requires the network on a runtime without the network does not run, not even for the rest of the work, because the failure this design removes is a step that started and did nothing.
A runtime that cannot narrow a capability to what was asked may run, and must say so.
Over-granting is computed from what the builder could render, per capability, rather than from a fixed table. Where a CLI supports scoped permissions, a tool set narrows to those programs and a source allowance narrows to those hosts, and the over-grant is empty even though prefix matching is not a security boundary. Where it does not, the over-grant is recorded.
The runtime does not choose the working directory. The executor owns it, creates it empty per step, lays the mounts, and removes it afterwards including on a panic. Every runtime is given the same directory in the same way, including the ones whose CLI takes a directory flag rather than inheriting a process working directory, because a runtime whose process working directory is the worker’s own resolves relative paths against the wrong tree the moment a step is allowed to run a program.
The input-file flag is derived from the mounts rather than passed by the caller: a caller with no single input file should not have to know that one provider reads that flag as text context and would misread a binary document through it. Such a flag feeds a file as context and does not give the agent a route to write it back, so EditInPlace on a runtime whose only file mechanism is that flag is not provided, and preflight refuses the step.
Only AgentExit::Completed publishes. A step killed on the timeout leaves a half-rewritten law behind, and publishing that risks a truncated file passing an article count and a schema check and being recorded as done. A non-completing exit discards the working directory and the chain fails; nothing partial reaches the bus.
Err(RuntimeError) means the agent could not be run. Ok(StepReport) means it ran, including badly.
Resource exhaustion is not only a spawn failure. The markers that identify it also arrive in the agent’s stderr on a non-zero exit, so the executor classifies stderr_tail on every non-completing exit and can raise ResourceExhausted from a report as well as from an error. Without that, moving non-zero exits from the error channel to the report channel would quietly stop the worker restarting when it should.
Everything protecting the worker from the subprocess is in one place, and no runtime reimplements it.
What the supervisor owns, and what must survive the move: the process group so a kill reaches the whole tree rather than the direct child, with the guard against signalling process group zero and the drop-time kill as a backstop; the RSS watchdog over /proc with its explicit degradation on non-Linux and the reason it is not a resource limit; the bounded stderr tail with re-logging and the cancellation of the drain task on every exit path; the fixed-block draining of stdout so a full pipe cannot deadlock the child, now feeding the event parser instead of being discarded; the stripped environment; the node heap ceiling, which is the first brake the memory watchdog is the second of; and stdin, which is closed or carries the instruction and is never inherited from the worker.
Token rotation across several subscription credentials is provider-specific and belongs to that provider’s builder. The environment allowlist is not, because it carries names for more than one provider and it protects the worker rather than the agent.
Capabilities are rendered onto whichever axes the CLI has. Where a CLI separates the set of tools offered to the model from the set of calls permitted without confirmation, both are set from the same grant: an undeclared capability is neither offered nor permitted. Offering a tool that is then refused at call time is how an instruction can prescribe work that never happens, so a grant renders on every axis the runtime has, and rendered is a list rather than a string.
A runtime that discovers configuration from the working directory and its ancestors, or that keeps session state in the home directory, has a second channel into the step that the mount list does not describe. The grant therefore also fixes which setting sources are active and whether session state persists, and those settings are part of what realize returns. A firewall stated only over files in one directory, on a runtime that reads instructions from three other places, is not stated.
Three layers, resolved per field, last one wins.
The memory ceiling sits on the runtime rather than on the step, because it describes a process and means nothing for a runtime that does not start one.
The profile is a checked-in TOML file built into the image. TOML because this is configuration; the corpus stays YAML. This adds a toml dependency to the workspace.
Running the qualifier on a different model is one line in this file, or one environment variable for a single run.
Per-step timeouts in the profile are ceilings rather than reservations, and the chain never spends more than the job allows.
admits runs before the first step. A chain that cannot fit must not spend three agent runs discovering it, and on a chain without step-level resume a None halfway through discards everything before it.
A four-role chain does not fit inside a job timeout sized for one agent run, and the job timeout cannot be raised past the orphan reaper’s threshold without the reaper killing healthy jobs. Both move together or the chain does not run, and moving them lengthens the time a stuck job blocks a serial queue. The numbers are in the decision list rather than here, because they are deployment values.
A linear sequence rather than a graph.
Native operations get the corpus root, because the checks that motivate the whole exercise read sibling laws and a bus of artefacts cannot supply them.
Linear because RFC-027’s chain is linear, and because parallelism buys nothing here: the roles are separate runs inside one job rather than separate job types, since three job types would clone one law three times and hit the per-provider rate cap three times for it.
Branching is routing, and routing is Rust rather than data:
The result is a flat list, and a flat list serialises. The provenance records what ran, including which steps the routing skipped.
Cheap and fallible before expensive. A chain orders every fallible native step ahead of every agent step. Without step-level resume, a failure after three agent runs pays for those runs again on the next attempt, and the source gate, the schema checks and the context assembly can all fail for free. The chain validator enforces the ordering.
Every agent step runs over a window of articles and never over a whole law. Round 1 of the enricher archive is the measurement: the Zorgverzekeringswet was translated in one run and its 434 leden hit the 64k output ceiling, so the law came back truncated and the run was lost. No model tier fixes that, because the ceiling is per response.
Three rules follow, and the first two come from RFC-026.
The window is a set of articles, never a fraction of one. An article is the unit of work there because leden are not independently readable, and a window that cuts an article in half hands the agent a second lid whose “in afwijking van het eerste lid” points at text it cannot see.
What works on an article travels with every window that contains it. The scoping rules of RFC-026 are per article and not per law, so they cannot be satisfied by putting the definitions in the first window and hoping. The placement path, the definitions of every enclosing container, the law-wide provisions, and the articles that modify anything in this window are assembled per window. That assembly is the largest input cost and the reason a window is smaller than the output ceiling alone would suggest.
A window may be one named article, and then it is not a walk. The ordinary window is a stretch of the document in numbering order, starting where the cursor of the previous run stopped. RFC-026 also asks for the opposite: enrich this one entry, because the hot path of a root names it, because a delegation it waited for has arrived, or because it came back wrong. --article 2.1.i on enrich-once states that entry and the run enriches it and nothing else.
Two properties keep that mode from corrupting the walk. The cursor stands still, since targeted work is not progress through the document and a run that repairs one entry must not push the walk past entries no agent has read; the termination guarantee of the walk is therefore the one it always had, ceil(entries / width) successful runs. And a number the law does not have fails the run with that number in the message, rather than enriching nothing, which reads from the outside exactly like a run that found nothing to do.
Enumerative articles may be split by lid. A definition article carrying forty begripsbepalingen is one article by numbering and forty independent statements by content, and the argument against splitting does not hold there: no lid modifies another. This is the one exception, and it is decided by the same is_definition_text test the coverage check already uses.
A judgement about the law as a whole cannot be windowed at all: name collisions between articles, one concept modelled two ways in two places, an output nothing consumes. Those are deterministic checks over the finished file rather than agent work, which is where they already sit. Where a law-wide judgement does need a model, it is map-reduce over per-article summaries and never a run that reads the law whole.
The window bounds the translation and not the gates behind it. The deterministic checks read the file whole, so a run over three articles is answered with findings about all thirty-five, and the repair rounds hand those on unfiltered. For a walk that is tolerable, because the run that owns those entries is the previous or the next one in the same series. For a targeted run it is a mismatch: the entry was named so the rest would be left alone, and the gate asks about the rest anyway. Scoping the gate output to the window is a change to the feedback prompt and is listed under the open decisions, not settled here.
EditInPlace exists for the same reason. A runtime that can only answer with one document rewrites the file it was given, so its output ceiling becomes the file size ceiling; a runtime that edits in place is bounded by the change and not by the law.
checkout_into is the chain’s way back to git. A chain that produces artefacts in a run directory and never writes them onto the paths the corpus expects produces nothing, so exports are part of the chain. The metadata sidecar, the result envelope carrying related legislation, and the generated feature files are all declared exports.
Per step the executor does five things: create an empty working directory; copy each mount to its declared path, with read mounts set read-only; let the runtime write its scaffold and run; on a completed exit, read back exactly the output mounts, hash them and publish them; and remove the directory.
Copying rather than linking, throughout. With whole-corpus mounting gone, the cost is a law file and its context, and the write-back rule depends on copying.
The write-back rule makes the boundary two-sided. Anything else the agent left in its working directory is discarded with a warning, so a step cannot smuggle context forward through an undeclared file, and a read mount that comes back changed is a recorded violation rather than a published artefact. Runtime scaffold is a third category next to mounts and agent output, named in the grant and exempt from both rules.
RFC-027 requires the adversary to receive the statutory text and the produced YAML and not the reading note, so that it re-derives the classification independently and therefore also catches the qualifier’s errors. That becomes a mount list:
Skills are artefacts with names and are mounted per step, which lets the qualifier run without the schema and without the generation skill. A skill mounted by no step is not present at all, so linking every available skill into the tree the agent works in stops.
What this enforces. The agent is not told where the withheld artefact is, it is not in the directory it was given, no tool of the step points at it, and the step’s own capability grant does not include search outside that directory. Nothing else in the chain hands it over, and a later edit that adds the mount fails a test rather than a review.
What it does not enforce. The agent runs under the same user as the worker with the same filesystem rights. An agent that decides to look outside its directory can read what that user can read. This is a context boundary between cooperating processes and not a security boundary, and the difference matters for one reason: an untrusted input document must never be handed to a step that can run programs, which is why deterministic extraction comes first and the agent is the fallback. Making the boundary hard needs a mount namespace or a separate user per step, which is an image change and a privilege model in the pod, and is out of scope here for that reason.
A static check keeps a later edit from opening the boundary by accident:
Per agent run the chain records: step id, role, runtime identity including CLI version and resolved model, the instruction digest, the digest of every mounted skill, the profile digest, the exit, the duration, the token and cost accounting, the grant including what was over-granted, the tool ledger, and the judgement of every criterion including the ones that were met. The met judgements are recorded alongside the unmet ones, because the question “why was this law accepted” is answered by them and by nothing else.
The token accounting was cheap to obtain and is hard to do without. Every provider reports its own consumption at the end of its stream, and the worker drained that stream and discarded it. Round 3 could therefore compare two variants on outcome and wall clock and on nothing else, which left the question of whether the context brief is expensive because the input grew or because the agent did more work entirely open. Input tokens, output tokens, cache reads and cost now come out of the closing object; cost is kept as an integer count of tenths of a cent, because money in a float rounds wrong once the numbers get large enough.
A truncated tail reports nothing rather than a partial figure, and a provider that reports no accounting does not read as zero cost. Both follow one rule: an absent measurement must not be indistinguishable from a measurement of zero.
The instruction digest covers the instruction and the skills that were mounted for this step, which is what varies per step. A missing skill mount is a Misconfigured failure rather than a warning, because a digest computed over the files that happened to be present is a digest that looks valid and means nothing.
These records go in a table rather than in a metrics endpoint. The metrics surface is a set of SQL aggregations over the job tables, exported by the admin service; the worker has no metrics endpoint and several replicas would need aggregating anyway. So the shape is a job_steps row per agent run, keyed on job and step, with the fields above, and a handful of aggregations beside the existing ones.
The record is not the only use of the table.
The spend guard counts runs. The hourly cap counts jobs started per provider on the assumption that one job is one agent run. A chain breaks that assumption in the direction that matters, letting the same cap through several times the model usage. The cap query moves to this table and counts agent runs.
Exit classes become queryable. A timeout and a memory kill both end up as an error string in the job result, which makes “step two times out on a third of the long laws” a search through logs rather than a query.
Judgement ratios are the alarm. A criterion that meets on zero runs out of hundreds is the shape of the failure in #1036, and as a ratio per step and criterion it is one panel and one alert rule beside the existing daily summary.
Over-granting becomes a number. A runtime that cannot narrow its permissions reports a non-empty over-grant on every run, which states as a measurement what would otherwise be an anecdote about two lanes not being comparable.
Job progress is written by the executor at step boundaries rather than polled from a file the agent writes. The executor knows which step is running without asking, and an agent that hangs stops updating a file it was going to write anyway. The progress value holds the step id, the index, the total and the start time, which changes the shape the frontend reads and needs checking against that contract.
No agent framework. No planner, no tool-call loop, no message history, no agent calling another agent. RFC-027 is explicit that agents never talk to each other and everything moves through artefacts. This layer orchestrates processes and files.
No sandbox. The capability set is a contract with the runtime and not a security boundary. Reading it as isolation is a mistake with consequences.
No abstraction over the model. No prompt DSL, no template language, no provider-neutral message format, no token accounting, no heuristics over refusals. The instruction is a String that Rust code produces. Where a runtime wants a system and user split, it splits on its own convention. Where a runtime needs an output schema to demultiplex several outputs from one answer, that is a property of the step and is declared there rather than being appended to the instruction outside the digest.
No configurable capabilities per environment. A step’s capabilities are a property of the step. Granting network in production and withdrawing it in development brings back the silent-nothing failure in a new form.
No step definitions in YAML for non-programmers. Steps are Rust values and configuration is TOML. A success criterion is behaviour that moves with the schema and with the checks, and that cannot travel through YAML in both directions. The rendered spec does serialise, one way, for the provenance.
No graph, no parallelism, no conditional steps as data. Routing is code and its output is a flat list.
No abstraction over git, the corpus or the job queue. A working directory is a directory. Who filled the checkout, how drift against the base is decided, and how the retry ladder works stay where they are.
The deterministic checks stay outside the abstraction. The check runner, the source gate and the assembler are library calls used by criteria and by native steps. They never become steps with capabilities, because our own code already has every capability.
No distribution across jobs. Every step of one chain runs in one job in one worker, for the reason RFC-027 gives and because of the uniqueness constraint on the queue.
No step-level resume, in the first version. Making a chain resumable means a durable cursor with the three properties the chunk cursor has: one small value, committed together with the work it describes, and validated on read so a stale value falls back rather than skipping work. That is a commit per step boundary and a question about what the corpus repository tolerates. The ordering rule buys most of it for nothing, and the boundary between the expensive phase and the cheap phase is the one place a checkpoint would pay for itself.
This does not reorder RFC-027’s sequencing, and it must not delay it. Items three and four of that sequence, the mechanical checks and the source integrity gate, address the two largest measured failure classes, are already written, and are not wired into the worker. They run against the existing seam and need nothing from this RFC. Building them first and this second is the order, because this layer on its own improves no translation: it changes what a step is allowed to be, not what any current step sees.
Where this RFC sits in that sequence is item five, separate runs instead of one session, and as a precondition for item ten, the adversary and the arbiter with lexical routing. Item five is reachable without the whole chain machinery, and stage one below is what it needs.
Stage 1: the runtime layer, with nothing above it changed. The agent module with capabilities, the supervisor, the command builders, the event parsers and the ledger. The existing subprocess helper stays as a thin wrapper that builds a one-step spec and calls a runtime. The three existing invocation traits keep their signatures and every existing test keeps running. This is where the shell boolean disappears: the three call sites pass a capability set instead, the document converter asks for the two extraction programs by name, the law structurer asks for them only on the raw-file branch, and enrichment asks for nothing. Behaviour is unchanged except that a runtime which cannot narrow its permissions now says so. Stage one is worth building on its own merits and is the majority of the value in this RFC.
Stage 2: the failure taxonomy, in two commits. The supervisor yields typed exits and typed errors. The first commit keeps the existing error display formats exactly, so the worker’s substring classifiers keep working, guarded by the test that pins those formats. The second switches the worker to the typed fields and deletes the classifiers, including the third terminal semantics for Misconfigured. Two commits rather than one, because a retry ladder that changes behaviour silently cannot be found again.
Stage 3: enrichment onto the chain, in one move. The enrichment orchestration becomes chain execution, the invocation trait disappears, and the inline gates become criteria. This cannot be staged, because the trait and its fakes are the same object: the enrichment test module alone holds ten implementations, and law and document conversion hold one each with their own recorded assertions. They become one scripted runtime.
This is not less code than the fakes it replaces. It buys sharper assertions: three existing tests assert on what the runner received, which pins the chunk loop, and those become assertions over a recorded request including the capability set, the property this RFC adds.
The base-action decision that compares the enrichment branch against the corpus is checkout work that happens before a chain begins, and it stays where it is.
Stage 4: law conversion and document conversion, later and separately. After stage one they already run on the runtime layer, so this can wait. Both traits disappear, both fakes become the scripted runtime, law conversion becomes a two-step chain with the repair round expressed as OnUnmet::Repair, and document conversion becomes a chain that starts with a native step, because deterministic extraction comes first and the agent is the fallback. That ordering is the pattern this design generalises.
What runs alongside. The chain lands next to the existing enrichment path, and the local single-law binary runs it against a real law before the worker notices. The worker keeps calling the existing entry point until the chain replaces it wholesale. The task-flow enrichment path is a second caller with its own working directory and no git checkout, and it moves at the same time as the corpus-wide one rather than after it.
What splits. The current enrichment configuration carries two things. Runtime, model, timeout and memory ceiling move to the profile; the article budget, the code commit and the branch stay in a settings struct. The provider override becomes a chain-wide runtime override.
What the queue key means. A lane is identified in the job payload and the uniqueness index is built on it. Per-step runtime overrides in a payload would make that key stop describing what the job does, so they are not allowed: overrides live in the profile, and a payload carries one lane identifier.
MarkChannel routes to, and RFC-031 the split between a marking and an open termAn exploration by Bureau Architectuur of the Dutch Ministry of Economic Affairs and Climate Policy into the possibilities of transparent, executable legislation.
GitHub repository
How it works
Stay informed
Roadmap (Dutch)
Documentation
Research
Bureau Architectuur
Ministry of Economic Affairs and Climate Policy