Skip to content

0.33.0

Choose a tag to compare

@github-actions github-actions released this 29 Aug 22:43
· 593 commits to main since this release

Added

  • regex: matchers work in a trace-spec/1 document; they used to be refused by name. Write args: {command: {regex: "(&&|\\|\\||;)\\s*\\w"}} and the checker runs it, with the same three-valued reading as every other matcher — an argument the call does not carry is still not a match, and a field a transcript does not record is still unk rather than a failure. A regex: searches the field, where a glob: has to be the whole of it, so anchoring is ^ and $ and yours to write; and ., +, | and ( are metacharacters in a pattern and literals in a glob. Nothing you have already written changed meaning: a glob: is still anchored, still literal, and still digests to the same value, so no specification was renamed by this. TRACE-SPEC-008 has not gone away — it still means this matcher cannot be run and now fires on a pattern the engine will not compile, at validation, quoting the engine's complaint, before a transcript is opened. A pattern that matches every text there is ({regex: ".*"}) is refused under text.matches for the same reason {glob: "*"} always was; {regex: "^$"} — the final message is empty — is not, because a run can fail it.

  • Parked on a credential no longer reads as actively worked. A blocker is typed by what would clear it and the type is the kind — credential-blocker, decision-blocker, third-party-blocker — so it costs a name and no release. protocol artifact list and protocol artifact board mark an artifact a blocker still stops with blocked: <type>, so a parked item and a moving one stop looking alike without anybody opening a file, and --format json carries a blocked_by list on every row. New verb protocol artifact blocked [--type <type>] answers the question a backlog cannot: what is stopped, by what type, and on which single item — grouped by the blocker, so five stories waiting on one decision arrive as one row with five lines under it rather than as five separate conversations. A blocker stops blocking when it reaches the end of its own ladder (protocol artifact move <blocker> --to cleared), which leaves the record in the journal instead of editing away the fact that anything was ever stuck. The six starting types — decision, review, credential, third-party, capacity, deploy — are written down in artifacts/kinds/blocker.yaml with what clears each, and the list is open: a procurement-blocker of your own works with nothing added to it.

  • A blocker can say which evidence it is withholding, and explain names it. A new optional withholds: key on a planning document takes an evidence kind — protocol artifact new credential-blocker ci-token --withholds test_result --relate blocks:story:x — and records why a required fact does not exist: the gate wants a test_result, the job that would produce one cannot mint a read-scope token. protocol artifact explain story:x prints blocked by credential-blocker:ci-token (credential), withholding test_result above the status history, so the audit question why is there no record is answered out of the store. protocol artifact validate refuses a withholds on an artifact that blocks nothing (missing_declaration) and a spelling outside the evidence vocabulary (undeclared_evidence_kind).

  • A story says which objective it serves, and validate holds it to that. A fourteenth relation, serves, points at an objective — a vision artifact — and nothing else (validate refuses a serves into any other kind). Where a store declares at least one vision, every proposed, approved or active story or task must serves one, and protocol artifact validate names each that does not; draft, implemented and archived are exempt, so nothing agreed before objectives existed is rewritten and nothing nobody has agreed to is charged. A store with no vision artifact has declared nothing to serve and is untouched by the rule. The objectives themselves are atlas ROADMAP.md's O1–O6, held here as vision:O1…vision:O6 (atlas ADR 0005). protocol artifact relate <id> serves vision:<objective> is the line.

  • protocol drive transition — the governor a native flow consults at a section boundary. b10x-harness workflow run walks a flow protocol workflow flow projected from a workflow, and that projection is an ordering, not a government: no guard travels. The loop asks a transition hook before a section is entered and after it leaves; this verb answers it from the engine — evaluate for enter, transition on a copy of the execution for leave — so a native walk is governed by the same documents that govern a driven run, with no crate dependency in either direction (harness design 0003 § 7, E2; atlas ADR 0004). Reads the loop's document on stdin; exit 0 proceeds, 2 refuses with {"reason": …} in the engine's words, anything else is a verb that could not answer, which the loop reads fail closed. --run <id> positions the engine on that run's snapshot over the store as it is now; without it, on the state the flow path names — a step node after its state, a retreat group <first>-to-<last> at its first state on enter and its last on leave. The root is the flow's own container: entering it proceeds, leaving it is the engine's answer from a run's cursor and proceeds without one (the first paid native walk was refused at enter root and ran nothing; fixed the same night). A section that came out failed is left alone. Decides only: writes nothing, takes no lock, and a consultation leaves a run's cursor byte-identical.

  • A stolen lock is in the taking run's record, and a lock refusal says what the holder is doing. protocol drive run --take-lock and protocol drive resume --take-lock now write the lock they superseded — the holder's run id, pid and host — into the taking run's own cursor as took_lock_from, so protocol drive status prints this run took the lock from pid 4711 of run AUTH-142/2 out of the record rather than out of a terminal's scrollback. It was built and printed before, and never persisted: the cursor had a printer for a field that was always empty. The theft is written on the way in, so a run that supersedes a lock and then blocks, breaks its store or spends its budget without executing a step still leaves it in the record; a later resume over a free lock never clears it, and a resume that supersedes again records the newer theft over the older. The refusal a live lock answers with now names the holder's state, read from the holder's own cursor.json and never from lock.json — a lock file is written once and the state moves after every step — and says state unknown when that cursor is missing or will not parse, rather than dropping the clause or crashing on somebody else's file. Beside it, tested rather than asserted: a resume against a lock another live run holds is refused and writes nothing; a lock naming another host is refused at the binary and --take-lock does not pass it; an approval pause is the lock released with the current pointer kept. The story is story:operator-resume-ux, and the change is the product of this repository's own governed run W4-3 on 2026-08-29, integrated from the run's workspace after the driver defects that run surfaced were fixed under this heading.

  • protocol artifact validate now says when a document claims a revision no write produced. A revision: higher than anything the event log records for that document is reported as its own finding — story:x claims revision 99, and no write produced it: 3 event(s) logged, the highest at revision 4 (event …) — with a forged list in --format json beside drift and deleted, and exit 1. Until now such a document was reported as ordinary drift, which reads as somebody edited a field and sends you to the log for a state that was never in it. The test is against the highest revision the log records, not against the number of events it holds: a store's history predates the event log, and an observation is an event at the current revision that writes nothing, so a document sitting under more events than its revision is not a forgery. This detects; it does not enforce. No write path refuses a forged revision and validate grew no refusal — refusing one means knowing who wrote the document, which is gap register D-3 (attestation by signature) and is still proposed. Unchanged: a document with no events at all is still only counted as predating the log (N document(s) predate the event log, pre_provider in JSON), whatever revision it claims.

  • A command step can name the task document its run was started from: {task}. It joins {run_directory} and {transcript} as the third and last placeholder a step map may write, and the driver expands it to the absolute path of the task the run was launched with — the one protocol drive run --task <file> named, or the one discovery found when no flag did, and the same path again on a resume. Until now a map could not say this run's task, so protocol specification evidence reached its own discovery and bound to the task project.yaml names: a run driven with --task <a path that is not the project's> decided another story's specification, or refused over it, while the run's own cursor said something else. drivers/development/default.yaml now passes --task {task}, so a driven run binds explicitly rather than by discovery. If you write step maps: a misspelled placeholder is still refused at load, and the hint now offers all three names.

  • A store write can say who made it, and a driven step says so by itself. AEP_ACTOR declares the actor every protocol artifact new, move, body, relate and evidence is journalled with — human:<name>, agent:<name>, service:<name> or system — so protocol artifact history and protocol artifact explain can tell an agent's move from a person's. Until now every write in a store said human:$USER whoever made it, which is why an approval recorded by a driven run was indistinguishable in the record from the operator's own. A value that does not parse is refused, naming the variable, never quietly replaced by $USER; unset, nothing changes. protocol drive sets it to agent:<execution id> on every process it starts for a step, and that is the same actor an approval is refused from as self-approval, so a run cannot approve its own work under the name it writes under. What it does not yet reach: an llm step's model session, whose environment metaharness constructs from a fixed allowlist rather than inheriting — that session's own protocol artifact move is still journalled as human:$USER, and closing it needs a flag on the metaharness side (story:the-store-knows-who-wrote-it).

  • An operator step can be answered by a named agent, and the run says who answered. protocol drive run --pause-on-approval --approver agent:<name> admits one named non-human actor's recorded approval at an operator step; a person's approval is admissible without being named, on every run, as before. The run still stops at the step — the approver records a granted approval against the run's snapshot while it is stopped — and the resume reads what arrived: the cursor records who answered (answered … in protocol drive status), an approval by the run's own actor (its task, its execution, the harness its llm steps run under) is refused as self-approval whoever named it, and an approval by an agent nobody named is refused naming the flag. With an approver named, a resume that finds no admissible approval stops again and says who would be admissible. Naming a person, system, a service or the run itself is refused before the run starts. Attestation is exactly as strong as the record: the approver is whatever producer the approval carries, and gap register D-3 (attestation by signature) stays proposed.

  • A run directory records which binary each command step actually ran. commands.jsonl holds one line per step attempt with the program the map wrote, the program that was spawned and which of the two it was; each step's .log now opens with the same fact. Substituting a binary silently is its own kind of lie, and a reader can now tell a step that used the driver's build from one that used something it found.

  • protocol drive run and protocol drive resume refuse before allocating a run id when a map's command steps say protocol and the driver cannot guarantee they get this build. It fires only where the driver cannot name its own binary and the protocol on its PATH is a different version; the refusal names both versions, and it names the fix correctly — cargo install --root ~/.local puts the binary where a session looks, while a driver-side PATH is the operator's own shell, so putting ~/.local/bin first in it is part of the answer.

  • A profile can say read the public channels, never a direct message, and one that forgets is refused instead of validating clean. network.read is now scoped by audience, the way deployment.create is scoped by environment. network.read:private covers a read of correspondence addressed to a bounded audience — a direct message, a group DM, a private channel, a mailbox, a ticket's internal comment; network.read:public covers material published to an unbounded one; unscoped network.read is still both, and every profile, principle and task that writes it means exactly what it meant before. aep/1 puts network.read:private in its approval floor, so allow: [network.read] on its own no longer resolves: the refusal names the entry that was forgotten and the hint says to put it in deny or require_approval. The line that fixes it is deny: [network.read:private] beside the grant. Membership is not the control — a token that can read a direct message is exactly the case a denial is for — and a harness that cannot tell which audience a read will reach refuses rather than guesses, without having to remember to: a request that does not say its audience asks for the wildcard, and network.read:public does not cover the wildcard. The driver's tool table asks the private question for the same reason, so a profile that may not read privately is offered no WebFetch/WebSearch, which no table can promise is public. Nothing here enforces the rule at a Slack API — a capability is a declared authorisation decision and the actor that honours it is the harness — but a profile that forgot the rule is now a validation error rather than a paragraph somebody was asked to follow (story:private-message-denial, adopter register row D-I2).

  • Both harnesses can be pointed at one gateway, which is what makes a harness comparison one. --claude-endpoint and --claude-model join the existing --b10x-endpoint/--b10x-model, so a harness: claude-code step and a harness: b10x step can run against the same model. Without this a comparison of the two arms measured the two models at least as much as the two harnesses, and no scorer could separate them afterwards. metaharness requires --credentials none alongside an endpoint — a child pointed at a foreign endpoint must hold no operator credential — so the driver passes it rather than making the caller remember. An endpoint with no model, or a model with nowhere to go, is not a gateway and is ignored: metaharness refuses each alone, and passing half of one would turn a flag mistake into a launch refusal several states into a paid run.

  • protocol artifact show <id> prints one artifact. Its frontmatter fields — id, kind, status, title, summary, owner, tags, relations, revision — and then its markdown body, verbatim. There was no verb for an id in hand: list prints the whole plan, board arranges it, history prints the event log, explain answers what made a status happen, and body writes. A driven session in run W4-3/1 typed show five times and was answered unrecognized subcommand every time, which is why the verb is called show. An id the plan does not hold is refused naming it. Read through the contract, so a markdown, SQLite, Postgres or hybrid plan gives one answer.

  • task audit-check is a step of the gate, fourth of twenty: seven units of .engineering/checks/, four seconds, re-resolving every file:line docs/guide/open-vocabulary.md cites. A commit that shifts a cited declaration now fails the gate instead of leaving a closed verdict pointing at a serde attribute — which is what had happened: eight doc-comment lines landed above pub enum EvidenceKind and the audit's citation rotted silently, because the suite that catches exactly this was not in the gate.

Changed

  • protocol workflow flow emits every state as a section. Each non-terminal state is a group named for the state, holding its steps as <state>-1, <state>-2, … in the map's order — one node when the map gave it one step or none — and a retreat is a group of those groups. It was a bare node for a state with one step or none, and b10x-harness workflow run asks its transition hook at a group boundary and nowhere else (harness design 0003 § 3): the fifth paid native walk of 2026-08-29 was consulted four times, all at root, and never about receive, specify or decompose. Now protocol drive transition is asked on both sides of every state. The document's header says so; b10x-harness workflow plan shows every state as a section, and the harness's committed fixture adp-default.projected.yaml is refreshed from this verb. A reader keyed on node ids sees receive-1 where it saw receive; the state a node is in has always been run.state, and still is.

  • Where a driven step may write is decided by the step map, not by the driver. A driven llm step's writes — Write, Edit, NotebookEdit on the vendor arm — are now answered from that step's own scope: in the step map: denied refuses any writer, partial-only refuses the two that replace a whole file and admits a targeted Edit, allowed admits all three, first matching rule wins. The refusal names the rule that matched and the globs the step may write, so the map is where you go to change it. Until now the same decision was a Rust function spelled in one vendor's tool names, which is why the planning store's protection existed for claude runs and for no other harness; the native arm has been given the identical rules as --write-scope since 0.29.0, and both arms now read one declaration. A step map that declares no scope: restricts nothing — an undeclared scope is a map that said nothing, not a map that said everything — so a map driving work that must not rewrite the planning store has to say so; both maps shipped here (drivers/development/default.yaml, drivers/development/checks.yaml) now do. What stayed in the driver is the one rule no scope can express: an Edit whose old_string or new_string crosses a planning document's closing --- is still refused, because that is a judgement about an edit's text rather than about its path. protocol drive hook, which the native loop spawns, now answers that content rule alone and leaves whole-file writes to the declared scope.

  • The approval floor accepts a broad grant that explicitly denies the floored entry. Until now any outright grant overlapping a floor entry was refused, whatever else the profile said, so allow: [network.read] with deny: [network.read:private] beside it was refused for granting something it had just forbidden. The floor now asks its own question — what does this policy decide about the floored capability? — and a deny of it is an answer, because a denial cannot be granted back by any later document. An approval gate on the narrow slice is still not accepted as that answer: the broad grant would keep deciding Allowed for every scope the gate does not name, and refusing that shape is what the floor is for. A denial of something else the floor also covers changes nothing — a floor on deployment.create for every environment is not discharged by denying production.

  • The unknown capability refusal now lists the scoped spellings too. It read … approval.request, deployment.create[:env] and deployment.rollback[:env]; it now names network.read[:public|private] in the same list, so every capability a document may write appears in the one vocabulary listing most adopters ever meet.

  • A green cell in protocol eval's table does not mean the same thing on every arm, and the arm word now says which. A clean store-integrity row — no store_broken, census.denied = 0 — means the call was refused on arm driven, where every tool call is answered at a seam, and only the model did not do it on arms raw and native, where nothing adjudicates a call at all. The two were printed in one column, in the same words, and read as the stronger of them. The rule is written where a reader of the table meets it: as a table of the four arms under the new Evaluation surface section of the CLI reference (which is also the first time protocol eval is documented there), as a line the text rendering prints under any table holding a native cell, and on --arm's own help. There is no new column — a column is a change to a printed table's format, and that is the operator's decision, not this release's (docs/design/native-arm-store-integrity-design-v0.1.md § 6 O1 and § 8 OQ4). The same note states the other half: denied: 0 is nobody asked me, not nothing was refused, and only one of those is a fact about the run.

  • A quoted metacharacter is an argument, not a composition. The rule refusing &&, |, ;, > and $(…) scanned the bare characters, so grep -n "StolenLock\|took_lock_from" crates/ — one invocation whose | belongs to grep — was refused. It surfaced within minutes of the readers being admitted, three times in one state of a live run: a tool admitted and then refused the natural way to use it, which tells a session two things and lets it believe neither. The check now tracks single quotes, double quotes and a backslash escape, and asks whether the metacharacter is outside both. $( and a backtick still compose inside double quotes, because there they still substitute; inside single quotes they are literal. It is not a shell parser and does not try to be.

  • A driven state that admits reading can now read at scale. repository.read renders Glob and Grep; Claude Code 2.1.247 offers neither, and its own error tells the model to search file contents with grep via the Bash tool instead — which the driver refused. So a session was told to do the one thing the driver denied, and run W4-3/1 spent 19 of its 215 calls discovering that and never searched anything. The shell now also admits grep, rg, ls, cat, head, tail and wc, and only when the state admits reading. It is not a general shell: sed and awk write, find has -delete and -exec, xargs and env run something else, and composition and redirection were already refused — which is the only reason a reader cannot become a writer here.

Fixed

  • A date written east of Greenwich is no longer a claim about the future, and one bad record no longer discards the whole evidence file. A bare calendar date in observed_at — observed_at: 2026-08-30 — is a day, and it meant midnight UTC — so a store at UTC+2 writing local calendar dates wrote a future instant for the last two hours of every UTC day: 20 of one adopter's 215 records, refused, and one refused record refused the entire document. A bare calendar date is now compared against the moment that day begins in the most-ahead timezone in use, UTC+14, so it is refused only once it is in the future for every writer on earth; the epoch-millisecond spelling keeps the exact comparison it always had, because a caller who wrote an instant meant one. The two spellings of one instant are therefore no longer the same value. Nothing is relaxed and nothing is clamped: a date that has begun in no timezone is still refused, no observation time is rewritten to the engine's clock, and the refusal is not downgraded to a warning. What changed is what it refuses — that record, named by file, by position in the file (record 12) and by the date as the writer wrote it, instead of an epoch pair and the document. protocol evaluate submits the file's other records, prints its evaluation and exits 1; a document whose every record is future-dated still fails. protocol evidence inspect puts every record to the same comparison, so the two verbs answer identically about one file — --at, which pins the comparison to the end of the named day rather than the wall clock, is the one place they still differ and the help now says so. examples/evidence-horizons-corpus/writers-day.yaml is the case, with the pair that carries it: two records naming one instant, one written as a day and admitted, one written as an instant and refused.

  • A review-result can be recorded with its body, and retired. Found by the second adopter (story:review-result-cannot-be-authored): protocol artifact new took no body, body refused the kind as immutable, and move --to archived — the one transition artifacts/lifecycles/review-result.yaml declares — was refused by the same guard, so a review recorded through the CLI was an empty record nobody could complete or retire. Now protocol artifact new … --from <path|-> writes the body with the record, for every kind; a move on an immutable kind is decided by its lifecycle alone, so active -> archived succeeds and the way back is refused by the ladder; body on one stays refused. protocol artifact --help no longer says an unwanted item is deleted with rm — it is retired with move --to archived — and validate's deletion finding names that command and the recovery.

  • protocol workflow flow --map no longer takes a step map and throws it away. The flag was accepted, its own help promised that each node would carry what a harness actually does in that state, and the projected document came out identical with it and without it — every node holding the state's name and summary and nothing a harness could run. It now carries the step into the node: a state with one step keeps one node and gains that step's fields, a state with several becomes a group named for the state whose steps are chained in the order the map wrote them, and a state the map is silent about is unchanged. An llm step travels as its prompt, context, scope — written <glob>=<word> in the map's own order, because first match wins — harness and description; a command step as its argv and the evidence running it establishes; an operator step as what it asks the person for. The header names the map and the pin it was written against, so a projection of development/default can be told from one of development/checks. A map pinned to another version of the workflow being projected is refused before anything is written, in the same words protocol drive run refuses it in. Without --map the output is byte for byte what it was, which is what the b10x harness's committed fixture walks.

  • A logged write that changed nothing no longer reads as a hand-edited revision. protocol artifact body handed an empty body records an update event at the next revision with changed: {}; validate compared the document's revision against the last event that changed something — the create, one revision back — and reported revision disagrees with event …@1 on a store nobody had touched by hand. Seen on a driven native run on 2026-08-29, where the driver's own validator step then exited 1 on every attempt and the run spent its budget in receive. Drift's revision check now reads the highest revision the log records, which is what the forged check already read; status and the written fields still fold from the last event that wrote them. crates/aep-backend-markdown/src/drift.rs.

  • protocol eval no longer tells you native is not an arm. A run manifest naming an arm this build does not have is refused by name, and the refusal lists the arms there are — but that list was written when there were three arms, and the commit that added the fourth never reached it. So a manifest with a typo in arm: was answered with is not one of the three arms this evaluation has: `raw`, `plugin`, `driven` while arm: native was, and had been, perfectly readable. The refusal now lists all four and states no count, so the next arm cannot make it wrong again, and a test checks the list against the enum's own variants rather than against a second hand-written one.

  • An approved specification of somebody else's story no longer lets your task start implementing. spec-driven and clean-room asked for kind: specification, status: approved and nothing more, which is a query over the whole artifact store — so in any store holding more than one piece of work, the guard before implement was satisfied by a specification of a different story. Run NATIVE-1/1 passed it holding zero approvals of its own. Both rules now bind to the work the task declares (relation: {kind: specifies, target: task}), and the row reads artifact specification (approved) which specifies this task. What to do if it now refuses you: your task document names the story or task it is for, under derived_from:, and the specification carries specifies: <that story> — that is the edge a driven run's specify state already writes. A task that declares no derived_from is matched only by a specification naming the task itself (specifies: task:<id>); the refusal says which of the two ends is missing rather than leaving you to guess.

  • The record saying your specification is satisfied is now about your specification. protocol specification evidence picked any in-force specification in the store, so in a store holding more than one piece of work it wrote the specification record — the one spec-driven reads as specification.satisfied before completion — about another story's document, and about one the guard before implement would itself have refused. It now selects by that guard's own rule: an approved specification whose specifies edge lands on the work the task declares. The task comes from --task <file> or from the project you are standing in; with neither in reach the selection is unbound and behaves exactly as it did. --artifact still says which specification to decide and no longer lets you name one that is not this task's — that is refused, naming what is declared and what your task said it was about — but it does still lift the status, so a draft can be asked whether it states anything a fact could decide. What to do if it now refuses you: the specification needs specifies: the story your task names under derived_from:, which is the edge a driven run's specify state already writes.

  • A run that walked past an operator step with nothing recorded now says so. Until now the pause advanced the cursor and the resume carried on, and a run nobody approved was indistinguishable in its own record from one somebody did — NATIVE-1/1 moved establish_verifiers -> implement holding zero approvals. The behaviour is unchanged for a person who moved the artifact the prompt named and resumed (the guard on the way out still decides); the report now carries one line saying the record holds nobody's answer.

  • A command step of a step map that says protocol now runs the build that is driving the run. A command step is spawned by the driver (crates/protocol-cli/src/drive.rs) with the driver's own environment, so the name resolved against whatever protocol came first on the operator's shell PATH. Run W4-3/1 hit a 0.28.0 install predating the property verb: protocol property evidence wrote nothing, the driver correctly reported nothing was observed, and the step spent its whole retry budget three times with the cause invisible in the message. The driver now spawns its own current_exe() for any step whose program's file name is protocol, which also guarantees the version agreement the run's evidence is recorded against. Every other program a step names — cargo, bash, git — resolves exactly as before.

  • A driven step on the native loop is told which programs it may start. harness-tools withholds run outright when no allowlist was supplied, which is that loop's rule everywhere — a tool outside the surface does not exist rather than being refused. The driver never passed one, so a session asked to record something in the planning store, whose only route is the protocol CLI, had no way to start a process: it spent 30 tool_search calls, 28 of them distinct, hunting for run, exec, shell, spawn and execute. The list is the same decision driven_surface enforces on the vendor arm — the CLI, plus the readers a reading state may use — rendered rather than re-decided, and the native rendering is the stronger of the two: a program not on it has no tool to reach it, where the vendor arm refuses the call after the model has spent the turn.

  • The tool audit asks each harness in the vocabulary that harness answers in. A vendor harness publishes one tool per act, so offered_tools is its answer; the b10x loop publishes three verbs over a catalogue — tool_search, tool_describe, tool_invoke — and states its reach as available_operations. Comparing a rendered catalogue against those three verbs reported every entry as missing: a run whose record published file.read, dir.list, search, file.write and file.edit was told, once per state, that it lacked all five. An audit that fires on a session holding exactly what it needs is worse than no audit, because the next true one is read as noise.

  • A shipped conformance spec now gates the outcome of the call it counts. conformance/trace/expectations.trace.yaml's created-through-the-cli is a tool.called row: it says a protocol artifact new was reached for, and nothing at all about what came back — so a recorded run whose only creating call errored satisfied it, and the eval passed on a store that got nothing. A second row beside it, the-creating-call-succeeded, decides the outcome over the same selector. It is a tool.error_rate at at_most: 0.99 — not every such call failed, deliberately not none did: a refused chained command line followed by a clean re-issue is correct behaviour under the plugin's own guardrails, and a zero bound would report the guardrail working as a gap. The scope names tools: [Bash, run] and operations: [command.execute, shell], which union, so the row decides on the native arm instead of going unk there. Against the committed fixtures it reads ok on both plugin-eval transcripts (0 of 2 calls failed) and ok on the driven honest step (1 of 2, rate 0.500).

  • The resume line the driver prints is a line that works. A stopped run printed resume with: protocol drive resume <run> (crates/protocol-cli/src/drive.rs), and that command re-read none of --map, --task, --pause-on-approval or --plugin-dir — so an operator who typed exactly what they were told got a different run, or an error. The run directory now holds a launch.json recording how the run was started, and resume fills in anything the caller left out; a flag still wins over it. A run started before this exists resumes exactly as it did.

  • A resumed run gets the budget the operator typed. --max-iterations was compared against the cursor's lifetime count, so a run that had already spent 25 iterations was budget-exhausted before evaluating anything — W4-2's first resume reported steps 0 run having done nothing, and no flag could have changed it. The bound is now on the call. The lifetime count stays in the cursor and the stop message reports both, because how far did this run get is a real question.

  • protocol drive refuses a run whose sessions could not reach the protocol CLI, instead of spending on one. A driven llm step reaches the planning store through that CLI and nothing else — the state's shell exists for it and admits no other program. metaharness constructs the child environment rather than inheriting it, so the session's PATH is $HOME/.local/bin:/usr/local/bin:/usr/bin:/bin and a target/debug exported before the command reaches the driver and never the model. Run W4-3/1 walked two states being answered exit 127, command not found and submitted nothing, for $1.03. The refusal names the constructed PATH and the one install that lands on it — cargo install --path crates/protocol-cli --root ~/.local, because cargo's own default is $CARGO_HOME/bin, which the session does not search either.

  • A driven run of a project loads that project's own plugin without being asked. Started without --plugin-dir, W4-3/1's sessions loaded no plugin and answered Unknown skill: planning to the first thing the step map asks for — while offering the operator's personal skills and tools, because a session with no plugin is not a session with no inventory. <project>/integrations/claude-code is now the last fallback after the flag and the environment variable. A flag nobody remembers does not fail loudly; it produces a run that walks, spends and records the wrong thing.

  • A driven session is told which task the run drives. A step map is written once and driven many times, so its prompt can only say read the task under .engineering/ — and a repository that has driven more than one run has several sitting there. Run W4-3/1 read task.yaml, which belongs to W4-1, described that objective instead, found its intake already in the store and reported the work done; the engine's cursor said W4-3 the whole time. Nothing was violated — the guards held, the store was untouched, every transition was the engine's — the run was simply about something other than its own audit trail said. StepContext now carries the task and the prompt names it, with what it is derived from, before the map's own words (story:governed-dogfood-run).

  • The audit's own checks read the store with this tree's build, not the ambient protocol. Four store helpers, H1 and F2 still resolved whatever was on PATH — here a 0.28.0 binary against a 0.32.x store, which is how five stories were reported as drifted that had not drifted. One resolved binary now, version-checked against the workspace, and exported so an inner run on a copy with no target/ reads with the same build rather than falling back to the stale one.