Repository navigation
0.33.0
Added
-
regex:matchers work in atrace-spec/1document; they used to be refused by name. Writeargs: {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 stillunkrather than a failure. Aregex:searches the field, where aglob: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: aglob:is still anchored, still literal, and still digests to the same value, so no specification was renamed by this.TRACE-SPEC-008has 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 undertext.matchesfor 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 listandprotocol artifact boardmark an artifact a blocker still stops withblocked: <type>, so a parked item and a moving one stop looking alike without anybody opening a file, and--format jsoncarries ablocked_bylist on every row. New verbprotocol 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 inartifacts/kinds/blocker.yamlwith what clears each, and the list is open: aprocurement-blockerof your own works with nothing added to it. -
A blocker can say which evidence it is withholding, and
explainnames it. A new optionalwithholds: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 atest_result, the job that would produce one cannot mint a read-scope token.protocol artifact explain story:xprintsblocked by credential-blocker:ci-token (credential), withholding test_resultabove the status history, so the audit question why is there no record is answered out of the store.protocol artifact validaterefuses awithholdson 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
validateholds it to that. A fourteenth relation,serves, points at an objective — avisionartifact — and nothing else (validaterefuses aservesinto any other kind). Where a store declares at least onevision, everyproposed,approvedoractivestory or task mustservesone, andprotocol artifact validatenames each that does not;draft,implementedandarchivedare exempt, so nothing agreed before objectives existed is rewritten and nothing nobody has agreed to is charged. A store with novisionartifact has declared nothing to serve and is untouched by the rule. The objectives themselves are atlasROADMAP.md'sO1–O6, held here asvision: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 runwalks a flowprotocol workflow flowprojected from a workflow, and that projection is an ordering, not a government: no guard travels. The loop asks atransitionhook before a section is entered and after it leaves; this verb answers it from the engine —evaluateforenter,transitionon a copy of the execution forleave— 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; exit0proceeds,2refuses 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 onenterand its last onleave. 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 atenter rootand 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-lockandprotocol drive resume --take-locknow write the lock they superseded — the holder's run id, pid and host — into the taking run's own cursor astook_lock_from, soprotocol drive statusprints this run took the lock from pid 4711 of runAUTH-142/2out 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 owncursor.jsonand never fromlock.json— a lock file is written once and the state moves after every step — and saysstate unknownwhen 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-lockdoes not pass it; an approval pause is the lock released with thecurrentpointer kept. The story isstory:operator-resume-ux, and the change is the product of this repository's own governed runW4-3on 2026-08-29, integrated from the run's workspace after the driver defects that run surfaced were fixed under this heading. -
protocol artifact validatenow says when a document claims a revision no write produced. Arevision: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 aforgedlist in--format jsonbesidedriftanddeleted, 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 andvalidategrew 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_providerin JSON), whatever revision it claims. -
A
commandstep 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 oneprotocol 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, soprotocol specification evidencereached its own discovery and bound to the taskproject.yamlnames: 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.yamlnow 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_ACTORdeclares the actor everyprotocol artifact new,move,body,relateandevidenceis journalled with —human:<name>,agent:<name>,service:<name>orsystem— soprotocol artifact historyandprotocol artifact explaincan tell an agent's move from a person's. Until now every write in a store saidhuman:$USERwhoever 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 drivesets it toagent:<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: anllmstep's model session, whose environment metaharness constructs from a fixed allowlist rather than inheriting — that session's ownprotocol artifact moveis still journalled ashuman:$USER, and closing it needs a flag on the metaharness side (story:the-store-knows-who-wrote-it). -
An
operatorstep 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 anoperatorstep; 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 grantedapprovalagainst the run's snapshot while it is stopped — and the resume reads what arrived: the cursor records who answered (answered …inprotocol drive status), an approval by the run's own actor (its task, its execution, the harness itsllmsteps 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 whateverproducerthe approval carries, and gap register D-3 (attestation by signature) stays proposed. -
A run directory records which binary each
commandstep actually ran.commands.jsonlholds 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.lognow 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 runandprotocol drive resumerefuse before allocating a run id when a map'scommandsteps sayprotocoland the driver cannot guarantee they get this build. It fires only where the driver cannot name its own binary and theprotocolon itsPATHis a different version; the refusal names both versions, and it names the fix correctly —cargo install --root ~/.localputs the binary where a session looks, while a driver-sidePATHis the operator's own shell, so putting~/.local/binfirst 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.readis now scoped by audience, the waydeployment.createis scoped by environment.network.read:privatecovers 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:publiccovers material published to an unbounded one; unscopednetwork.readis still both, and every profile, principle and task that writes it means exactly what it meant before.aep/1putsnetwork.read:privatein its approval floor, soallow: [network.read]on its own no longer resolves: the refusal names the entry that was forgotten and the hint says to put it indenyorrequire_approval. The line that fixes it isdeny: [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, andnetwork.read:publicdoes 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 noWebFetch/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 rowD-I2). -
Both harnesses can be pointed at one gateway, which is what makes a harness comparison one.
--claude-endpointand--claude-modeljoin the existing--b10x-endpoint/--b10x-model, so aharness: claude-codestep and aharness: b10xstep 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 nonealongside 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:listprints the whole plan,boardarranges it,historyprints the event log,explainanswers what made a status happen, andbodywrites. A driven session in runW4-3/1typedshowfive times and was answeredunrecognized subcommandevery time, which is why the verb is calledshow. 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-checkis a step of the gate, fourth of twenty: seven units of.engineering/checks/, four seconds, re-resolving everyfile:linedocs/guide/open-vocabulary.mdcites. 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 abovepub enum EvidenceKindand the audit's citation rotted silently, because the suite that catches exactly this was not in the gate.
Changed
-
protocol workflow flowemits 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, andb10x-harness workflow runasks itstransitionhook 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 atroot, and never aboutreceive,specifyordecompose. Nowprotocol drive transitionis asked on both sides of every state. The document's header says so;b10x-harness workflow planshows every state as a section, and the harness's committed fixtureadp-default.projected.yamlis refreshed from this verb. A reader keyed on node ids seesreceive-1where it sawreceive; the state a node is in has always beenrun.state, and still is. -
Where a driven step may write is decided by the step map, not by the driver. A driven
llmstep's writes —Write,Edit,NotebookEditon the vendor arm — are now answered from that step's ownscope:in the step map:deniedrefuses any writer,partial-onlyrefuses the two that replace a whole file and admits a targetedEdit,allowedadmits 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 forclauderuns and for no other harness; the native arm has been given the identical rules as--write-scopesince 0.29.0, and both arms now read one declaration. A step map that declares noscope: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: anEditwhoseold_stringornew_stringcrosses 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]withdeny: [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 adenyof 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 decidingAllowedfor 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 ondeployment.createfor every environment is not discharged by denying production. -
The
unknown capabilityrefusal now lists the scoped spellings too. It read… approval.request, deployment.create[:env] and deployment.rollback[:env]; it now namesnetwork.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 — nostore_broken,census.denied = 0— means the call was refused on armdriven, where every tool call is answered at a seam, and only the model did not do it on armsrawandnative, 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 timeprotocol evalis documented there), as a line the text rendering prints under any table holding anativecell, 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: 0is 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, sogrep -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.readrendersGlobandGrep; Claude Code 2.1.247 offers neither, and its own error tells the model to search file contents withgrepvia the Bash tool instead — which the driver refused. So a session was told to do the one thing the driver denied, and runW4-3/1spent 19 of its 215 calls discovering that and never searched anything. The shell now also admitsgrep,rg,ls,cat,head,tailandwc, and only when the state admits reading. It is not a general shell:sedandawkwrite,findhas-deleteand-exec,xargsandenvrun 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 evaluatesubmits the file's other records, prints its evaluation and exits 1; a document whose every record is future-dated still fails.protocol evidence inspectputs 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.yamlis 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-resultcan be recorded with its body, and retired. Found by the second adopter (story:review-result-cannot-be-authored):protocol artifact newtook no body,bodyrefused the kind as immutable, andmove --to archived— the one transitionartifacts/lifecycles/review-result.yamldeclares — was refused by the same guard, so a review recorded through the CLI was an empty record nobody could complete or retire. Nowprotocol 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, soactive -> archivedsucceeds and the way back is refused by the ladder;bodyon one stays refused.protocol artifact --helpno longer says an unwanted item is deleted withrm— it is retired withmove --to archived— andvalidate's deletion finding names that command and the recovery. -
protocol workflow flow --mapno 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. Anllmstep travels as itsprompt,context,scope— written<glob>=<word>in the map's own order, because first match wins —harnessand description; acommandstep as its argv and the evidence running it establishes; anoperatorstep as what it asks the person for. The header names the map and the pin it was written against, so a projection ofdevelopment/defaultcan be told from one ofdevelopment/checks. A map pinned to another version of the workflow being projected is refused before anything is written, in the same wordsprotocol drive runrefuses it in. Without--mapthe 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 bodyhanded an empty body records anupdateevent at the next revision withchanged: {};validatecompared the document's revision against the last event that changed something — the create, one revision back — and reportedrevision disagrees with event …@1on 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 inreceive. Drift's revision check now reads the highest revision the log records, which is what theforgedcheck already read;statusand the written fields still fold from the last event that wrote them.crates/aep-backend-markdown/src/drift.rs. -
protocol evalno longer tells younativeis 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 inarm:was answered withis not one of the three arms this evaluation has: `raw`, `plugin`, `driven`whilearm: nativewas, 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-drivenandclean-roomasked forkind: specification, status: approvedand nothing more, which is a query over the whole artifact store — so in any store holding more than one piece of work, the guard beforeimplementwas satisfied by a specification of a different story. RunNATIVE-1/1passed 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 readsartifact 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, underderived_from:, and the specification carriesspecifies: <that story>— that is the edge a driven run'sspecifystate already writes. A task that declares noderived_fromis 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 evidencepicked any in-force specification in the store, so in a store holding more than one piece of work it wrote thespecificationrecord — the onespec-drivenreads asspecification.satisfiedbefore completion — about another story's document, and about one the guard beforeimplementwould itself have refused. It now selects by that guard's own rule: an approved specification whosespecifiesedge 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.--artifactstill 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 adraftcan be asked whether it states anything a fact could decide. What to do if it now refuses you: the specification needsspecifies:the story your task names underderived_from:, which is the edge a driven run'sspecifystate already writes. -
A run that walked past an
operatorstep 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/1movedestablish_verifiers -> implementholding 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
commandstep of a step map that saysprotocolnow runs the build that is driving the run. Acommandstep is spawned by the driver (crates/protocol-cli/src/drive.rs) with the driver's own environment, so the name resolved against whateverprotocolcame first on the operator's shellPATH. RunW4-3/1hit a 0.28.0 install predating thepropertyverb:protocol property evidencewrote 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 owncurrent_exe()for any step whose program's file name isprotocol, 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-toolswithholdsrunoutright 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 theprotocolCLI, had no way to start a process: it spent 30tool_searchcalls, 28 of them distinct, hunting forrun,exec,shell,spawnandexecute. The list is the same decisiondriven_surfaceenforces 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_toolsis its answer; the b10x loop publishes three verbs over a catalogue —tool_search,tool_describe,tool_invoke— and states its reach asavailable_operations. Comparing a rendered catalogue against those three verbs reported every entry as missing: a run whose record publishedfile.read,dir.list,search,file.writeandfile.editwas 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'screated-through-the-cliis atool.calledrow: it says aprotocol artifact newwas 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 atool.error_rateatat_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 agap. The scope namestools: [Bash, run]andoperations: [command.execute, shell], which union, so the row decides on the native arm instead of goingunkthere. Against the committed fixtures it readsokon both plugin-eval transcripts (0 of 2 calls failed) andokon 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-approvalor--plugin-dir— so an operator who typed exactly what they were told got a different run, or an error. The run directory now holds alaunch.jsonrecording how the run was started, andresumefills 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-iterationswas compared against the cursor's lifetime count, so a run that had already spent 25 iterations wasbudget-exhaustedbefore evaluating anything — W4-2's first resume reportedsteps 0 runhaving 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 driverefuses a run whose sessions could not reach theprotocolCLI, instead of spending on one. A drivenllmstep 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'sPATHis$HOME/.local/bin:/usr/local/bin:/usr/bin:/binand atarget/debugexported before the command reaches the driver and never the model. RunW4-3/1walked two states being answeredexit 127, command not foundand submitted nothing, for $1.03. The refusal names the constructedPATHand 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 answeredUnknown skill: planningto 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-codeis 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. RunW4-3/1readtask.yaml, which belongs toW4-1, described that objective instead, found its intake already in the store and reported the work done; the engine's cursor saidW4-3the 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.StepContextnow 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,H1andF2still resolved whatever was onPATH— 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 notarget/reads with the same build rather than falling back to the stale one.