Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog and the project uses Semantic Versioning.
Unreleased
[0.13.1] — 2026-09-24
Changed
- The landing copy now leads with the harness as the control plane under your coding agents, rather than with the rule-detector loop, and the spend capability is retitled so it no longer claims savings the cost benchmark does not show. (#764)
Fixed
- The grade-bash timing test no longer fails under the Python 3.9 floor on a loaded machine. It
takes the best of five runs rather than one, bounds a 100 KB command by a tenth of the
PreToolUse timeout
harness syncregisters rather than a fixed 0.2 seconds, and adds a growth check against a quarter-size command so a quadratic scan still fails. (#743)
[0.13.0] — 2026-09-23
Added
-
The acceptance runner passes
CLAUDE_CODE_OAUTH_TOKENinto each case’s disposable home and the smoke tier counts it as a reachable credential, so an operator on a Claude subscription runs a Claude Code round on the tokenclaude setup-tokenmints instead of on-demand credit (#672). -
Where the qualification targets run and how each is provisioned is now written down, because a release session had to rediscover it every time.
docs/compatibility.mdnames the binary source, the login and the host for each of the four CLI targets: every round to date ran on one Mac, Codex on macOS is the binary bundled in the ChatGPT desktop app logged in with a ChatGPT session, and both Linux targets run in a container on that Mac built from the newscripts/linux-target.Dockerfile, which pins both clients. The runbook’s new target-hosts section gives the commands in order. The smoke tier’scredentialscheck now takes--targets, which a qualification round passes through, and fails at once, naming the target, when a client is offPATH, the Codex login is missing or the Docker daemon a Linux target needs does not answer; with no targets named it checks what this host can run and reports a macOS target on a Linux host as skipped. The runbook also records that the acceptance runner does not yet carry a Codex session login into its disposable home, so Codex evidence can only be produced by hand until it does. (#408) -
scripts/cost_bench.py replay --tagruns a pinned git ref of this repository, and is repeatable, so--tag v0.12.0 --tag v0.13.0 --model <id>measures two harness versions against bare in one invocation and writes a history row for each, labelled with the version and commit of the ref it actually ran rather than of whatever harness happens to be installed. Each ref is checked out with its history intact and projected by its ownbin/harness syncinto a config directory of its own, run with a temporary HOME as well as an explicitCLAUDE_CONFIG_DIR: the profile the owner runs under is neither read nor written, and the owner’s identity and stance selection — which a sync renders out of~/.config/agent-harness/config.json— stay out of the measurement, so two tags are asked the same question. Every ref is resolved before the first launch, a ref that does not resolve is a named error rather than a quietly missing row, and both temporary directories go even when a run in the middle of a tag’s schedule raises. The harness arm’s fence admits the pinned checkout its profile links into, and--dry-runprints the schedule per tag and syncs nothing.--spend-capapplies to each tag’s schedule on its own. A profile’s credential is keyed on its absolute path, so a temporary one is not signed in: name a signed-in--harness-configfor the tag to be synced into when the run is meant to spend. That profile must start with no harness files in it, none of the names the sync writes may be a link out of it, and it cannot servecandidatein the same run; after each tag the sync is taken back out of it — exactly what its own manifest records is removed andsettings.jsonis put back atomically from a copy — while anything else written during the run stays and credential files are never copied, rewritten or deleted, and the profile is checked afterwards so a tag whose sync recorded elsewhere stops the run with the leftovers named rather than stranding the next one; the pinned checkout is readable, never writable, from the arm, and every refusal about the target is decided before the first launch (#599). -
A qualification round names two capability classes per target rather than one: an execution class,
standardby default, for the worker that runs the scripted cases and writes the findings, and an assessment class,strongby default and a floor rather than a preference, for the reader that assesses the observations. Now that the required cases are scripts, running them is reading JSON and writing a file, which is not work the strong tier is needed for; assessing what they observed is, and the published procedure requires a reviewer. Both classes are written into the evidence record and the round record, so the evidence says which class produced an observation and which class read it, and either can be moved per target with--execution-class TARGET=CLASS. Three pairs are refused before any client is launched: an assessment class weaker thanstrongor one the adapter does not map at all, and a cheap execution class that resolves to the assessment class’s own model, whether because the table maps both to one identifier, spells one model two ways, or maps the cheap class not at all so that it resolves upward — in each case the worker that produced the evidence would be its only reader. The two identifiers are compared through the usage ledger’s own normalisation rather than as strings, so a date-stamped id and a bare alias of one model are one model. An unmapped execution class beside a mapped assessor is disclosed in the record rather than guessed at. The routing is written to the durable log before the first case and to the round record before the smoke tier, so a killed round still says which classes were running, and--from-progressrefuses a log that mixes two routings rather than merging them. The resolution reads the adapter’s table and not a personaltiersoverride, because a round runs from a frozen clone. The saving is the issue’s estimate and not a measurement: 230K–590K output tokens per round, most of it authoring rather than judgement (#338). -
Every required acceptance case now has a driver in
scripts/native_acceptance.py, so--dry-planno longer says of any case that it is not automated yet. Nine of them are new here; most were prose in an evidence record that a worker re-implemented by hand each round, which is where most of a round’s orchestrator tokens went. They includespawn-confinement, which #291 added to the required set: it spawns a framework’s review work with nosubagent_typeat all, carrying only the sentences the integration descriptor itself declares — read from the descriptor rather than restated, so a driver cannot keep passing after the descriptor stopped naming them — and reads the refusal beside an ordinary spawn that must still run, because a guard that refuses everything would otherwise read as a pass. Each new case emits the observation string a reviewer assesses, and each returnsunverifiedrather than a pass when the behaviour it is about was not observed: a runtime that writes no subagent record is a named gap, and a model that declined a turn is not a control.gate-invalidationdelivers its Stop events to the runtime’s own coordinator rather than paying for eleven client turns, and says so in its observation. The provisioning and driver that used to live only in each round’s scratch copy are committed asscripts/qualification_provision.pyandscripts/qualification_round.py; the clone comes from this repository’s own object store, and the pinned BMad installer, for the optional integration suite a minor release runs by hand, is the only step that reaches the network. The runner also gained a Codex configuration home — its ownCODEX_HOME, thecodex exec --jsoninvocation and the rollout layout — derived fromadapters/codex/worker.py,policy/hooks/usage-log.pyand the documentation. No Codex round has been driven through it, so every Codex verdict is reportedunverifiedwith its observation kept until an operator compares one against a hand run and passes--home-confirmed, which is the disciplinepermission-controlsalready owes. Confirmation is per target, because one surface agreeing with a hand run says nothing about another, and on an unconfirmed surface an assertion that did not hold isunverifiedtoo rather thanfailed: what is in question there is the reading, not the harness (#336). -
Every decision-provider call now leaves a
kind: "decision"row in the usage ledger beside the session rows, carrying the decision point, the mode, the status, the requested and the returned model id, the pack and request hashes, the judgment and severity labels, the deterministic outcome and the one anactmode would have reached, the token counts, the latency and the session that asked — and none of the state it sent, no prompt, no file path and no environment value, because the row is built key by key from that list and reads nothing else. The counterparty is part of what goes out in the request and could be a path, so a row keeps it only when it matches therepo:<name>/<branch>slug the ledger already derives, within a bounded length, and keeps a short digest of anything else. Exported over OTLP the row travels underharness.decision.*, its price included, because itsinputandusdin the columns a session’s land in would have a backend counting the harness’s question as session spend. A judgment costs tokens and holds up a turn, and until now neither figure was anywhere:harness usage --by providerprices the calls frompolicy/prices.jsonlike any other row and reports the latency distribution beside the statuses, which are separate columns rather than a success rate — an answer the provider abstained from is not the same event as no answer at all. A call whose usage nobody reported ispartial, so it is named in the unpriced footer rather than read as free, and the rows are counted on that report alone: their tokens were spent asking a question, not by the session, so adding them to a day or a repo would charge a session for a bill it did not run up. The write is an append under the ledger lock rather than the rewrite a session record does, so a call inside a hook’s budget does not re-read and rewrite the whole file.harness doctornow also names the model every request pins and what the last call returned, since a provider may answer on a model the harness did not ask for and a report priced at the requested one would then be priced at the wrong rate.telemetry.decisionsis one switch over both ledgers: off, neither row is written (#139). -
A labelled corpus for the eleven detectors this repository writes itself, and a
corpusjob besidetestthat scores it.tests/fixtures/detector-corpus/holds thirteen synthetic transcripts and the labels over them, five positives and five near-misses per detector bar the one whose positive costs two hundred searches, written bybuild_sessions.pybeside them;scripts/detector_corpus.py --floor 0.9runs both that corpus and the one inside the vendoredruleprobewheel through the whole registry and exits non-zero when a detector’s precision or recall falls under the floor, or when a detector has no labelled example at all. Every rowharness usage --rulesprints now has a measured precision and recall rather than a hit count of unknown quality. Two detectors measure 0.83 precision: any basename holdingid_rsais a hit forsecrets/git-add-secret-file, so a runbook named after a key is one, andautonomy/denied-by-gradematches the grade hook’s signature anywhere in a Bash result, so a grep that prints it is one. The floor stays where it is and each miss is recorded in the corpus with the score and the floor it was measured against, so an improvement or a regression both fail the job until the record is updated, while a run at a lower floor leaves the record dormant rather than stale (#522). -
harness integration check|apply <name>is the surface for a declared framework integration. It reads the template directory, the install destination, the presence probe and the skill surface frompolicy/integrations/<name>.json, so the CLI holds no framework name, and the session-start drift notice probes that descriptor’sdetectpath instead of a directory literal in the hook. Itsdetect,templates,destinationandskill_rootsmust be relative paths with no..segment, becauseapplywrites under one of them.harness bmad check|applyis kept as an alias, and installed override files are unaffected either way (#349). -
Every session row carries
raw_vs_deduped, the measured size of the usage deduplication: the per-line sum of the four token fields over the deduplicated total the row reports, taken over the same records — the session’s own and those of the subagent files folded into it. The totals are corrected once per message id at that id’s largest figure, and the raw sum used to be discarded, so a session whose transcript repeated every response and one that repeated none reported the same figure with no way to tell them apart.harness usageprints the ratio for the window as a footer figure besideunpriced— each row’s ratio weighted by the deduplicated tokens it contributed to the columns above, so the figure is the window’s raw sum over its counted sum — and the OTLP export carries the row’s own as araw_vs_dedupedattribute. A Codex session row, whose runtime reports cumulative snapshots rather than a figure per record, readsunknownrather than1.0, which would claim a measurement nobody made; a subagent row, a worker row and a row written before this release carry no such key, which the report reads as unknown and counts in the footer (#518). -
harness decisions evalreplays the labelled rows of the decision log through a question pack and reports how closely the judgment tracked them, so a provider can be measured before it is trusted. The evaluated set is the real log — the input a hook judged, the deterministic answer it gave and the outcome the session later showed — replayed inshadowmode against a recorded fixture, so an ordinary run opens no socket, writes no ledger row and needs no credential. Question packs are versioned: a pack carries an id, amajor.minor.patchversion and the hash of its content, is frozen at construction so nothing holding one can rewrite a criterion between the hash being taken and the request being built, and the provider puts the id and version on every ledger row beside the request hash. The dev and held-out split is seeded by the hash of the decision point and the capped input a request actually carries, rather than byrandomor by the row’s hash of the uncapped text, so a re-run reproduces it, new rows do not reshuffle the old ones, and one identical request cannot sit on both sides — which is refused outright rather than reported. Thresholds are fitted per decision point on the dev split and only the held-out block is evidence; there is no global default, because a cutoff optimal on one workload does not transfer, and a point with no labelled dev case is reported unfitted rather than given the shipped 0.8 as though it had been measured. The fit is scored under the provider’s own semantics — below the threshold the deterministic answer is what is compared against the label, since that is what the harness would have done, and scoring an abstention as a miss would drive every fit to the lowest confidence in the set. The report names accuracy, the deterministic answer’s own accuracy as the baseline to beat, how many cases the provider could have changed at all, the confusion by label, agreement with the deterministic answer, a calibration table over the confidence with its expected calibration error and a bootstrap interval drawn from hashed indices, the flip rate over repeated passes, tokens, cost per 1,000 decisions where a price was given, the returned model ids, and anunusableblock in which an unavailable call, an error and an abstention are each counted and none is ever a pass. It carries no clock, no absolute path and no input text, so two runs over one log are byte-identical and the file can be sent on. What the labels do not prove is in the report itself:grade-bashrecords a row only where the harness asked or denied,ranis a user approving something they were asked about rather than proof the prompt was unneeded, andnot_rundoes not tell a refusal from an interrupted turn.--liveis the opt-in path and needs an explicit request ceiling;--budget-usdneeds a price beside it, because none is published here and a dollar ceiling nobody can convert is not a ceiling;--replayand--livetogether are refused rather than silently ordered; and a live run under an emptygovernance.jev.state_fieldsis refused as pointless, since every request would then differ in nothing (#138). -
The
jevdecision provider is opt-in per decision point, and sends only what a configuration lists.governance.jev.modesets a default andgovernance.jev.modes.<point>overrides it for one of the points the decision ledger already names:offcalls nothing,shadowcalls and writes the ledger row where neither the model nor the user sees it,adviseadds the judgment and says what acting on it would have done, andactmay turn anallowinto anaskand nothing else. Every mode defaults tooff, so a configuration written before this existed makes no request, and an unknown mode, decision point or field is refused atharness config setrather than at the first call. Two controls sit outside the modes:~/.local/state/agent-harness/jev-disableddisables every call while it exists, read per decision so the switch needs no restart or configuration change, and the allowlistgovernance.jev.state_fieldsis empty by default and coverscommandandsummaryalone: a file path, a prompt, an environment value, tool output or assistant prose has no field to go in and is never built into a request. A listed field whose text matches one of the shared secret shapes is dropped whole rather than masked, and free text is withheld entirely when that pattern list cannot be loaded; redaction recognises the shapes it knows, which is why the allowlist is two fields rather than a free vocabulary. The request timeout defaults to two seconds inside the hook budget, and a request and token ceiling bound the session rather than the process: a hook is a new process per event, so the counters are kept in the state directory keyed by session id under the existing lock, and a spend file that cannot be read or written leaves the in-process count standing rather than failing a decision. Each call writes one ledger row carrying the mode, the judgment label, the severity level, the deterministic outcome and the outcome acting on it would have reached, so ashadowanswer can be compared against the decision it did not change; labels only, never the state. Every failure path still fails open to the deterministic decision, andharness doctorprints the mode per point, the allowlist, where the kill switch lives and whether a credential variable is set — by name, never its value (#137). -
One Bash command in twenty that the harness allows is now kept in the decision log as a sampled negative: a
grade-bashrow withdeterministic_answer: allow,sampled: trueand thesample_rateit was drawn at. The graded rows are all prompts, so a check that may only tighten an allow into an ask had nothing to measure its false alarms against. Only an allow the harness itself gave is sampled — a command it left to the runtime may still be prompted on or refused, and on Codex an approval is dropped from the hook output — and a confirmed command belongs to the prompt it answered. Which commands are kept is each command’s own hash rather than a random draw, so the same corpus samples the same commands on every machine and a measurement over these rows is reproducible; the sample is therefore of distinct commands, not of invocations, and the row count cannot be weighted by the rate to estimate how many commands ran. The rows carry no outcome and no match key, are passed by at SessionEnd rather than closed asnot_run, andharness usage --by decisioncounts them on agrade-bash (sampled)line of their own, so a point’s outcome rates and unlabelled share are unchanged. The text is redacted before it is capped — assignment values, credential flags, every secret shape the rule detectors match and the home directory as~— and the row’s hash is over the redacted text, so a short secret cannot be recovered from the hash beside it.telemetry.allow_sample_ratesets the rate and0turns it off, as doestelemetry.decisions: false(#386). -
A spike record measures what the Claude Code Workflow tool does to the delegation guards. A script’s
agent()calls produce noAgenttool call, so band routing, the brief guard and the constrained-role refusal never see them, and a script can run a read-only harness role in session by naming it inagentType; the usage ledger does record every workflow agent, but those rows carry no tool use id, so the reroute join is empty for all of them. The record names the two changes the code needs.docs/spikes/2026-09-22-workflow-tool-band-routing-and-ledger.md(#540). -
A framework integration descriptor,
policy/integrations/<id>.json, names a framework, the release it is pinned to, how its spawns are recognised, which harness role each spawn maps to, and the input roots a confined worker needs; BMad Method 6.12.0 is the first tenant. The spawn hook now classifies a native spawn against those descriptors instead of trusting thesubagent_typethe model wrote, so a review layer re-issued as an unnamed subagent is refused with the same isolated-worker instruction a namedreviewerspawn gets, and the refusal names the read roots that worker needs. Theharness-role:line in a routed brief goes back to being an optimisation rather than the thing confinement depends on. Recognition is corroborated: one of the framework’s own layer names as the spawn type is enough, a path out of its routed text counts only with one of its sentences beside it, and two of those sentences are enough on their own, so the fix-up brief after a review, a brief that edits the override templates and a brief that quotes a single line of them all still run. A classified refusal is deliberately not remembered for the session, because that memory matches by prefix and similarity and one wrong classification would go on refusing the corrected brief. A descriptor that will not parse or will not validate is announced once per session and logged rather than skipped in silence.spawn-confinementjoins the required qualification cases, with a false-positive check, and the catalog records what a descriptor still cannot recognise (#291). -
scripts/native_acceptance.pydrives thepermission-controlscase, which was qualified by hand every round: it syncs the manual, unacknowledged bypass, acknowledged bypass and auto postures, reads the permission mode each one wrote into the client’s own settings, and asks for the same one-command file write under each, pre-approving no tool so that the posture is what decides the call. The acknowledged bypass is judged bybypass_verdict, landed uncalled in 0.12.0, against the mode the client reported for that turn, so the three outcomes stay apart: a turn the policy blocked, a turn the model declined on its own judgement, and a turn that completed. A decline isunverifiedrather than a failure, which was the defect the driver had to be written around, and each posture’s reading is kept as it is made, so a later posture that cannot be observed reports the earlier ones rather than erasing them. Three real client turns recorded under those modes — result, transcript record and the sentinel state each left — are the tests’ fixtures, so the suite still launches no client, anddocs/releasing.mdrecords the comparison against a hand run that the first live round owes before this verdict is trusted (#404). -
A session that was already running when
harness syncinstalled the band workers starts routing unnamed spawns as soon as it can resolve them, instead of waiting for a new session. Claude Code announces a reload to the session it happened in, as anagent_listing_deltaattachment on the transcript, so the spawn hook reads that over a bounded tail, keeps what it found in the session record so routing survives the delta scrolling out of that read, and routes to a worker the session’s start-time record predates when a later delta names it. The record stays the floor: an absent or unreadable transcript, a listing never seen in either place, and a session that reloaded nothing all route exactly as they did before, so a reroute still never turns a spawn that would have worked into one that fails. The pricing hook reads the same answer, anddocs/spikes/2026-09-22-registry-reload.mdrecords the eight sessions this was measured in, including the headless ones that never reload (#263). -
scripts/smoke_tier.pyruns the repository’s deterministic pre-qualification checks as one command that spends no model turn: the acceptance runner’s self-tests against recorded transcripts undertests/fixtures/transcripts/, the documentation-link check, the credential-reachability probe, and the disposable-home sync, projection-drift and lifecycle checks. Three of the four defects the 0.11.0 qualification round recorded were deterministic plumbing faults of exactly this kind, each found part-way through a paid round that then had to be run again. Every check is bounded by a timeout, so an unauthenticatable environment is reported as an error rather than as a three-hundred-second hang, and a check that could not run isunverifiedand never a pass. The tier is additive and never qualification: it observes no client, and the run fails if anything it ran wrote undercompatibility/evidence/or into the catalog. CI runs it as asmokejob that the branch ruleset does not require, anddocs/releasing.mdrecords that it is advisory until it is decided whether a red tier may block a freeze (#334). -
A
jevdecision provider answers thedecide/record/learncontract over the network, in the standard library alone, because the vendor SDK needs Python 3.10 and five packages where this repository’s floor is 3.9. It validates a question pack ofchoice,booleanandscoreanswers before anything is sent, refusing achoicequestion that offers no explicitunknownoption: the service cannot abstain, so a pack without one leaves a model that cannot answer no way to say so but to guess. A request is bounded at 64k tokens, and its state plus the longest question at 32k; a response that is malformed, incomplete or carries a field nobody asked for is an error and never a judgment with the bad parts dropped; a budget of requests and tokens is checked before each call and charged after it. A judgment may turn anallowinto anaskand may never widen a decision, and every path with no usable answer — no key, a timeout, an exhausted budget, an unparseable body, an unexpected exception — returns the deterministic provider’s decision unchanged with the reason inrule_matches. Each call records the status, the requested and returned model ids, the pack hash, the request hash, the usage and the latency to the decision ledger, and never the state. Answers are not deterministic across identical requests, so nothing here promises otherwise. The endpoint must behttpsand the opener holds no handler for any other scheme, because a bearer key goes out with every request; a request is charged to its budget as it is sent rather than when it succeeds, so a refusing endpoint cannot be retried without limit; andharness decidesuppresses the ledger row, because a reporting command changes nothing. The client is inert unless a caller constructs it withlive=True; the opt-in configuration, per-decision-point modes and the sentinel file are #137. The endpoint, the default model id, the token ceilings, the response shape and the HTTP status mapping are taken from the vendor’s documentation and have not been verified against the live service from this repository, which is what the one opt-in live request in the acceptance criteria is for (#136). -
The compatibility matrix carries a
tier restrictionrow saying, per client surface, whether the delegation stance’s model-tier ceiling is enforced, advisory or absent, and names the file behind each state. It is derived from atier_restrictionentry inadapters/<runtime>/capabilities.jsonrather than written into the rendered docs: the Claude Code CLI and VS Code surfaces readenforced, becauseclaude/hooks/tier-agent-spawns.pyrewrites a spawn asking for the strongest class by name. Every Codex surface readsadvisory: the coordinator runs there and a CodexAgentcall still passes the role, marker, evasion and brief checks, but the tier rewrite sits behind aruntime == "claude-code"gate inlib/harness_core/lifecycle.py. The plugin-marketplace install readsadvisorybecause it installs no hooks at all. The generated note states whatenforceddoes not cover — the session’s own model, which the harness never writes; adelegationvariant other thantiered; and a class table mapping fewer than two models — and thedelegation-tieringskill now links to the row instead of restating it (#520). -
scripts/smoke_tier.pyruns the repository’s deterministic pre-qualification checks as one command that spends no model turn: the acceptance runner’s self-tests against recorded transcripts undertests/fixtures/transcripts/, the documentation-link check, the credential-reachability probe, and the disposable-home sync, projection-drift and lifecycle checks. Three of the four defects the 0.11.0 qualification round recorded were deterministic plumbing faults of exactly this kind, each found part-way through a paid round that then had to be run again. Every check is bounded by a timeout, so an unauthenticatable environment is reported as an error rather than as a three-hundred-second hang, and a check that could not run isunverifiedand never a pass. The tier is additive and never qualification: it observes no client, and the run fails if anything it ran wrote undercompatibility/evidence/or into the catalog. CI runs it as asmokejob that the branch ruleset does not require, anddocs/releasing.mdrecords that it is advisory until it is decided whether a red tier may block a freeze (#334). -
BMad story files now carry the design.
scripts/bmad_issue_sync.pywrites each new work item from a typed template for its kind, with the issue link, parent and state in a managed block thatrefreshrewrites while keeping the rest of the file byte for byte. A newupgradesubcommand converts legacy stubs without loss, carrying amendment sections over verbatim, andaudit --delivery N, run by the requiredissue-ownershipcheck, fails a pull request whose own story leaves a required section unfilled. Legacy stubs pass that check with a notice until they are upgraded. (#620) -
BMad’s sprint and build workflows now have a status surface that nobody maintains by hand.
python3 scripts/bmad_issue_sync.py sprint-statusrenders_bmad-output/implementation-artifacts/sprint-status.yamlfrom the issue map and the story files, deriving each item’s status from its GitHub state and its story’s depth check.new,reserve,refreshandupgraderegenerate it whenever they write,auditfails while it drifts, and thebmad-buildandbmad-sprint-planningcustomizations point at the command instead of editing it. (#623)
Changed
-
Pull requests to
mainland through a merge queue, and changelog entries after 0.13.0 are one fragment file per change underchangelog.d/. Every pull request workflow also runs onmerge_group, and the issue-ownership and landing-copy checks read the queued pull request’s number from the queue branch, since a queued entry whose checks never report stalls the queue.scripts/release_notes.py --changelog <version>assembles the fragments into a version section in a stable order, andbin/harness lintfails a branch that changesbin/,lib/,adapters/,primitives/,policy/,docs/orscripts/without a fragment or a<number>.none.mdwaiver. Every branch used to edit the same Unreleased section, so any two in flight conflicted there, and the up-to-date requirement cost a rebase and a full CI rerun per landing. (#337) -
An isolated role worker is no longer handed the harness checkout as a read root and pointed at the whole skill corpus. It carries the shared policy as its system text and is mounted only what that policy tells it to open: the skills the resolved rules and stances name, and copies of the
docs/*.mdfiles they cite. The set is derived from the text itself, so a stance that stops citing a skill stops paying for it, and a worker is never told to obey a rule whose reference it cannot reach. A role adds what its body assumes but the shared text never names with askills:line —design-loopfordesign-judge,allforplanner, whose body tells it to read the skills the plan will name — and a name that resolves to no shipped skill fails the run rather than quietly removing that authority. Measured as what is mounted rather than what a run reads, a review layer went from the whole checkout, about 1,073,900 estimated tokens of text, to about 30,800, and the corpus it was offered as skill authority from all 31,600 tokens to the 26,600 the policy cites. Each run records the figure undercontextinstatus.json— policy, reference and total against a 50,000-token budget, counted with the approximationharness lintapplies to always-loaded context — recorded and not enforced, because what a worker is shown is fixed by its contract before any brief is read. On Claude Code the narrowing is enforced by the restricted file tools; under Codex’s read-only sandbox it is instruction text, as that runtime’s declared input roots already were (#335). -
The BMad override templates ask each review layer to launch only once the previous layer’s worker has exited. This is correctness before economy: a role worker that is still running publishes no token count, so a round with four layers in flight cannot be held under a cap it cannot measure, which is how an observed 450,000-token cap became 985,000 (#335).
-
A release no longer runs a third-party framework’s own workflow.
bmad-workflowleavesrequired_casesand is replaced byframework-spawn-routing, a generic case that builds a fixture recipe out of whateverpolicy/integrations/declares and drives the spawn hook with it: a recipe layer is refused whether it is spawned unnamed, as a generic subagent or as a band worker, the refusal offers exactly the descriptor’s declared input roots as the isolated worker’s read roots, and one cheap turn confirms that an unnamed spawn still routes to the cost variant’s default band worker at that row’s class and effort with the budget sentence, and that a null variant rewrites nothing. The BMad run becomes an optional, non-gating suite run once per minor release on one target, documented indocs/bmad.mdanddocs/releasing.md; the offline template and surface tests stay in CI, because they are what catches an upstream rename. The catalog now carries two limitations in place of one: that no framework’s workflow is exercised natively in a qualification round, and what descriptor-driven confinement still cannot recognise. Generic task continuation moves off the framework page todocs/task-continuation.md, and the framework-named asides in the design-loop skill, the handoff workflow, the delegation-tiering skill and a spawn-hook test are gone (#349). -
claude/settings.template.jsonno longer carries a hooks block. Dispatch has been single-coordinator for some time — a sync registers one command per lifecycle event andruntime_template()takes that registration fromlib/harness_core/lifecycle.py— so the eleven per-policy entries the file still listed were replaced unread at every sync, and an entry added there by hand would have been silently discarded.docs/how-it-works.mdnow describes the model: why one process per event rather than one per policy, where precedence is decided, and how the coordinator fails closed. Two places that still described registration as conditional are corrected with it:docs/preferences.mdsaid theplan-ceremonystance decides whether the plan-card validator is registered, where it decides whether the validator runs, andclaude/OWNERSHIP.jsonnow says in the manifest itself that a hook id’sstanceandvariantname when a policy acts, never whether it is registered. No installed settings file changes, because what sync wrote was already the coordinator registration (#521). -
The standing context every session loads is 1,005 estimated tokens smaller, 7,524 to 6,519 on
scripts/cost_bench.py static. Thescannableoutput style keeps all nine of its rules and loses the worked examples and the register table, 1,528 tokens to 862; the fifteen skill and eleven agent descriptions lose their capability restatements and keep every condition and literal user phrasing a session selects on, 2,326 tokens of listings to 1,980. Because a description is the trigger mechanism, those phrases are now frozen intests/test_description_trigger_phrases.py, which fails when one is dropped rather than reworded, and a new skill or role must declare its own. Before-and-after rows are indocs/benchmarks.md; the third change the issue names, deferring action-gated rule text behind the hooks that fire on the act, is framed as an unrun spike indocs/spikes/2026-09-22-deferred-rule-text.mdand nothing about it is implemented (#430). -
/plannow enters plan mode, writes its Review Card into the plan file the runtime designates, and finishes throughExitPlanMode, so the native plan pane is the review surface and the native approval is the gate. The typedbuildreply was a convention no tooling could observe, and a plan written straight to disk reached no plan view at all. Because the runtime fixes the filename before any content exists and plan mode permits no other write, the naming waits for approval:/planthen renames the file to a topic slug — refusing to overwrite an existing name — and invokes/buildwith that path, which the builder commits into its worktree so the plan reaches the pull request./buildworks from the path or the issue number it is given and never searches for a plan, because a plan found by modification date is as likely to be a stale one a checkout touched./planasks before entering plan mode, since entering it is the user’s call, and a runtime with no plan mode — or a user who declines it — keeps the previous behaviour as an explicit branch: write the file named for the topic, open it for the reviewer, close on the build line (#439). -
Native qualification evidence is now invalidated per target rather than per repository. Each client’s evidence is checked against the shared runtime source plus its own runtime’s adapter directory, so a fix confined to
adapters/codexno longer costs the Claude Code targets of a qualification round, and the reverse holds; a shared-source change, an unmapped runtime or a record that states no scope still invalidates everything, so the narrowing fails closed. The catalog declares the per-runtime directories and each evidence record states the path set it was validated under. The narrowing stops at the files shared code reads whatever the runtime —bindings.json,capabilities.jsonandworker.py, whichharness tiers, stance coverage andharness role run --runtimereach for either adapter — so those are declared in the catalog and still invalidate every target; onlyhook.pyis private to its runtime. A new test parses the runtime source and fails when a loader reaches an adapter file the declaration does not cover. Per-case scoping is not included: it changes a v1 stable interface and waits on an owner decision (#333). -
The delegation rule now states that subagents never message a peer, and the builder role says what a blocked builder does instead: stop, finish what does not depend on the answer, and return the question under Deviations for the caller. A delivered message bills as a typed prompt on the receiver and again on the sender when the reply lands, and turn count is what the delegation arithmetic is sensitive to, while the measured coordination wins in the literature all come from mediating writes at write time rather than from agents conversing;
delegation-tieringcarries that reasoning with the three papers cited. Session-to-sessionSendMessagebetween human-facing sessions is unchanged (#539). -
The BMad governance text that every planning workflow loads now splits authority between the issue and its story file, routes each SDLC step to its BMad skill, and requires every pull request to keep the planning corpus current. The build workflow treats a work item’s story file as its spec and writes the design back into it. (#621)
-
The field scan credits claude-md-doctor and RuleReceipt, which now bind a check to each rule and report from Claude Code transcripts whether it was followed, and narrows the measured-rules uniqueness claim to what they do not do: the lint that refuses an unmeasured rule, detectors scored against a labelled corpus with a precision floor in CI, per-variant grouping, and Codex as well as Claude Code. The research run for the scan carries both sources as a dated amendment. (#710)
-
Step 8 of the qualification procedure now says what a headless client can show about a session started before the band workers were installed: its session record never widens to them and its spawn still succeeds. Such a session is continued by resuming it, and the resumed process loads the workers from disk and announces them, so routing its spawn to an announced worker is correct; a reroute to a worker that neither its record nor that announcement named is still a defect. The old sentence asked for a session that was never rerouted, which no headless turn can show. (#733)
Fixed
-
The stop gate releases a turn after eight consecutive red blocks in each session, even when another session is stopping in the same checkout. It kept one count per checkout and reset it whenever a different session stopped, so a Remote Control host session and an interactive one in the same checkout zeroed each other’s count and the gate never let either go. The count is now kept per session, and a session silent for a day is forgotten (#611).
-
scripts/cost_bench.py replay --tagno longer counts the CLI’s own synced skill packs underskills/synced/as leftovers of a tag’s sync, so a run with two tags into one named profile reaches its second tag instead of stopping after the first (#649). -
harness lintno longer reads untracked files under.agent-harness/. That directory stays unignored so a handoff can read its plans, so any session’s local note naming a project outsidedocs/turned the lint red, and the stop gate with it, for every other session in the same checkout. Tracked files there are still linted, and staging a note brings it back into the lint before it can be committed (#605). -
Remote Control hosts managed by
harness remote-controlnow reuse their environment across a restart. Claude Code 2.1.280 reads the folder’s bridge pointer only whencreateSessionInDiris on, and every host was launched with--no-create-session-in-dir, so each restart registered a fresh environment and eachSIGTERMarchived the host’s sessions. The flag is gone; heal’s pointer rewrite keeps 2.1.280’s twoparkedProjectThreadSessionIdskeys;installon a changed agent adopts the running host’s environment, writing the pointer and stopping the host withSIGKILLso its archive path never runs;healre-queues this Mac’s disconnected sessions throughbridge/reconnect, at most once per session every ten minutes; and aremote_control.foldersentry may be an object with its ownspawnandenv, so each workspace root gets a host that loads its ownCLAUDE.md, skills and hooks (#603). -
The
delegationstance, the shared role descriptions and the refusal a nativegathererorreviewerspawn receives now carry one sentence word for word: a read-only role runs throughharness role run <role>, because confinement is read roots and return shape rather than the absence of write tools, which is also whybuilderis exempt and spawns natively. A session that followed the stance used to spend a refused call discovering a rule none of the three texts stated, and the refusal’s reason for exempting the write-capable role was nowhere. A test holds the three copies together, so the sentence cannot drift in one of them (#304). -
The credential probe answers for a variable holding something that is not a path, where asking the filesystem about it used to raise and carry the value into the error’s own message — a service account document pasted into
GOOGLE_APPLICATION_CREDENTIALSprinted its private key. An unusable value is now treated as a file that is not there, and the reason names the variable and never the value (#334). -
The usage feed tells the orchestrator when its own session has grown past the posture’s fresh-session threshold. A long session’s cost is mostly the context every further turn re-reads, and the turn line, which reports output tokens, showed none of it. A new
session_nudge_atswitch in the cost sidecar lists context sizes in whole tokens, resolved overextendslike every other switch; onUserPromptSubmitthe feed reads the newest response’s input tokens plus its cached prefix and, at a crossing, adds one line naming the size, the threshold and the advice to finish the task, write the handoff and start fresh. It is said once per threshold rather than once per turn: the thresholds already said are kept in the session’s state file, and survive the reader starting over on a transcript whose identity changed, so a resume does not repeat them. A context that falls back under a threshold, which is what an in-place compaction does, arms that threshold again. Nothing is blocked.frugalships 80,000 and 120,000,balanced120,000 and 160,000, andmaxnothing at all; those are starting points chosen against a 200,000-token window rather than measured figures, and the follow-up to this issue replaces them with sizes read out of the ledger. Codex raises noUserPromptSubmitevent and declares the nudge uncovered (#321). -
docs/spikes/records the measurements a decision was taken on, starting with the in-run budget nudge: whether a running subagent should be told mid-run how its spend compares with its soft budget. Measured on one machine’s ledger, 2 of 89 budgeted subagent runs overran, the excess was 4.5% of subagent output, and in the larger sample of runs whose brief carried no budget the median overrun was discovered with one tool call left — too late to act on — so nothing is built and the record says which numbers would change the answer (#322). -
A
jevdecision provider answers thedecide/record/learncontract over the network, in the standard library alone, because the vendor SDK needs Python 3.10 and five packages where this repository’s floor is 3.9. It validates a question pack ofchoice,booleanandscoreanswers before anything is sent, refusing achoicequestion that offers no explicitunknownoption: the service cannot abstain, so a pack without one leaves a model that cannot answer no way to say so but to guess. A request is bounded at 64k tokens, and its state plus the longest question at 32k; a response that is malformed, incomplete or carries a field nobody asked for is an error and never a judgment with the bad parts dropped; a budget of requests and tokens is checked before each call and charged after it. A judgment may turn anallowinto anaskand may never widen a decision, and every path with no usable answer — no key, a timeout, an exhausted budget, an unparseable body, an unexpected exception — returns the deterministic provider’s decision unchanged with the reason inrule_matches. Each call records the status, the requested and returned model ids, the pack hash, the request hash, the usage and the latency to the decision ledger, and never the state. Answers are not deterministic across identical requests, so nothing here promises otherwise. The endpoint must behttpsand the opener holds no handler for any other scheme, because a bearer key goes out with every request; a request is charged to its budget as it is sent rather than when it succeeds, so a refusing endpoint cannot be retried without limit; andharness decidesuppresses the ledger row, because a reporting command changes nothing. The client is inert unless a caller constructs it withlive=True; the opt-in configuration, per-decision-point modes and the sentinel file are #137. The endpoint, the default model id, the token ceilings, the response shape and the HTTP status mapping are taken from the vendor’s documentation and have not been verified against the live service from this repository, which is what the one opt-in live request in the acceptance criteria is for (#136). -
telemetry.completion_claim, off by default, records the agent’s completion claim on astop-gatedecision row: the last 2 KiB of the turn’s final assistant message, read from the transcript at Stop because the Stop payload carries no assistant text, with the hash over the uncapped message. Verifying what an agent said it had done against the gate result needs the two on one row, and until now the row held only the gate. It is its own switch, and off, because it is the only field in the decision log that holds assistant prose; with it off the row is byte for byte what it was. The claim is the turn’s own: the scan stops at the user prompt that opened it, so a turn that ended in a tool call claims nothing rather than repeating the previous turn’s words. The read is a bounded tail, so it costs the same on a transcript of any size, and where there is no claim the row carries a null one beside acompletion_claim_missnaming why — a runtime that supplied no path reads differently from evidence that is gone (#387). -
Every replay-benchmark row records
cache_miss_ratiobeside its cache-normalised cost: the share of the run’s prefix the provider re-wrote rather than served, summed over every turn the run opened. The arithmetic is the oneharness usage --by prefixapplies to a ledger row, and is imported from that module rather than restated, but the two figures answer different questions and a fan-out run will differ: the replay counts a subagent thread’s fresh prefix as part of what the run cost, where the session figure subtracts it.benchmarks/history.jsonlandhistory.mdcarry each arm’s mean of it, so a candidate that buys fewer tokens by re-writing its prefix more often is visible in the history rather than hidden inside the dollars. A run whose CLI output carries no per-turn cache figures, or any one of whose turns reports its usage without them, isnull, never zero, since zero is a run that held its whole prefix (#497). -
Three faults in the qualification scripts, each of which would have cost a paid round to find. The round’s clone passed
--shared=falsetogit clone, an option that takes no value, so every provision exited;bidirectional-handoffsent the same--revisiontwice expecting a refusal second, wherelib/harness_core/tasks.pyrefuses only a revision that has been spent, and it sent the writing runtime as a contract field the same module rejects, so the case could never have passed; and the round driver left a previous target record in place, so a runner that exited before writing--outreported the older round’s passes as this round’s. A previous record is now moved aside first and an absent one is every caseunverified, a target or smoke tier that runs past the deadline is recorded and carried rather than raised,--print-envquotes every value it prints, a round directory inside any checkout or worktree of this repository is refused, and aclonedirectory this script did not create is refused rather than deleted.hook-compositionnow matches text only thegrade-bashhook writes rather than the stance name, which a model can echo without any hook having decided, andcustom-stancenames the resolved variant by its bytes where a surface copies it instead of linking it (#336). -
The credential probe answers for a variable holding something that is not a path, where asking the filesystem about it used to raise and carry the value into the error’s own message — a service account document pasted into
GOOGLE_APPLICATION_CREDENTIALSprinted its private key. An unusable value is now treated as a file that is not there, and the reason names the variable and never the value (#334). -
An assistant transcript record whose
messageis not an object is skipped rather than read as one. Such a record holds no usage, no model and no content blocks, and reading it aborted the scan of the whole transcript, so one malformed line cost the session its entire row; the subagent reader beside it has always skipped the same shape (#518). -
An assistant transcript record that carries no message id is deduplicated by its
requestIdrather than counted once per line. Every such record used to open a slot of its own, so a runtime or version that writes one id-less response several times — as the streaming lines of one API call — inflated the session total and its turn count without any bound. A request id names one call, so it deduplicates unscoped by file: the same call written into both a session file and a subagent file is one response, and a call whose other records do carry a message id joins their slot, in whichever order the files are read, instead of being billed twice. A record with neither id is unknown rather than a duplicate and is left undeduplicated; session and subagent rows carryidless_recordscounting how many such records their totals include, the session’s own and those of the subagent files folded into them, so a row without the field is known to have been deduplicated whole. Rows from transcripts whose records all carry message ids are unchanged (#519). -
The Remote Control sessions read checks that its capped page arrived newest-first. The endpoint takes no sort parameter, and its page is ordered by
last_event_atrather thanupdated_at, so the order is asserted on arrival and a page that is not descending is refused: under the fifty-row cap the rows such a page dropped are unknown rather than merely old, and the session a host lost minutes ago is exactly the one another order would hide.harness remote-control statusprintsnot checked (page order unknown)for a refusal, which no longer reads like an account with nothing lost, and both it anddoctornow sayin the newest 50when the account has more sessions than one page (#526). -
The repository’s own copy states the figures its code holds. The landing copy said nineteen detectors where the registry holds seventeen, six from the vendored engine and eleven written for these rules, and a new test derives that count from the rule pack and fails when
README.mdorproduct.jsondrifts from it.docs/field-scan.mdanddocs/caught-in-the-act.mdno longer say the conflict engine is empty (four constraints ship and lint fails on them), that the always-loaded cap cites no source (it quotes the memory documentation and the measured standing context) or that there is no labelled corpus: the vendored wheel ships one, it scores six of the seventeen detectors, and the remaining eleven and the precision floor in CI are #522.benchmarks/static.jsonand theharness lintcontext line each name the set they count, which differ, andbenchmarks/oracles/hook_ids.pysays its module count is the count at the task’s pinnedparent_sharather than at HEAD. The always-loaded line ratchet is now the line cap itself: the binding cap is tokens, the line cap is the secondary guard, and a third budget four lines below it only obscured which one binds (#516). -
A model id the price table does not list is unpriced, where an unlisted variant of a listed family used to inherit that family’s rate. Inheritance under-bills a premium variant by a multiple and prints a known-low figure as a known one —
gpt-5.5-prois $30/$180 wheregpt-5.5is $5/$30 — which was the one place the ledger broke its own rule that an unknown is never zero. Ids now resolve by exact match after normalisation, which additionally drops a release suffix (a date stamp, a reseller’s-v1:0, an@date), so every dated and cloud reseller spelling still reaches its family entry and no recorded row changes price. To price a variant, add it topolicy/prices.jsonor override it underpricesinconfig.json; until then it is counted in theunpricedfooter and carries noharness.usdattribute (#517). -
The
spawn-confinementqualification case now reads the harness decision log for the refused spawn’s session, so a framework spawn the hook refused is observed even when a headless client does not repeat the refusal in its answer. The client’s text is kept as a second signal, and when it carries the refusal its wording is still checked. (#702) -
Three scripted native-acceptance cases now drive
bin/harnesswith the contract it actually has. The role-confinement case passes the client’s own--runtimetoharness role run, the bidirectional-handoff case writes its task contract to a file and passes that path toharness task save --input, whose help now says it takes a path, and the migration-uninstall case changes a harness-owned setting by hand before uninstalling, so its expected exit 2 and “user changes preserved” are what it observes. A new test drives each case’s call through the real CLI. (#703) -
The
gate-invalidationacceptance case now reads the stop-gate hook’s state record at the repository’s resolved path, where the hook writes it, so a disposable home under a symlinked temporary directory such as macOS’s no longer reports the gate as never recorded. Its probe repository also ignores the gate’s own run counter, which had made every green run look like a tree changed during the gate. (#704) -
The
cost-postureandframework-spawn-routingqualification cases no longer fail when a headless resume reroutes an unnamed spawn to a band worker. Headlessclaude -p --resumeruns as a new process that loads its agent registry and announces the restored workers, so the spawn hook routes on that announcement as designed. The case now reads the session record and the transcript’s agent listing. It fails only when the record widened on resume, when a reroute went to a worker nothing named, or when the spawn did not run. (#705) -
The native acceptance runner now refuses a target whose platform is not the host’s, naming both, before any case runs; it used to stamp the record’s platform from the target spec, so a Linux round driven on a Mac wrote the Mac’s outcome as a Linux record. The round driver’s plan says where each target runs, and a round skips a target meant for another host, reporting it as not run here. (#708)
-
Three native qualification cases now observe what happened instead of reporting
unverified.role-confinementparses the JSON worker recordharness role runprints, and reads the result from itsresult_path.bidirectional-handoffreads the writing runtime from the task record itself and asks the reading session for it by name.spawn-confinementreads the session transcript to tell a refused spawn from an allowed one and from one the model never attempted, and says which. (#716) -
The
spawn-confinementnative case now makes the model attempt the spawn it classifies. Its brief quoted nothing and gave the descriptor’s “read that file” no referent, so a model asked which file was meant instead of calling the Agent tool. The spawn prompt now opens with a sentence naming the file, keeps the descriptor’s own sentences verbatim and still names nosubagent_type, and the brief says the quoted text is the subagent’s. (#718) -
A qualification round given no
--modelnow passes each target its routed execution model, so the cases run on the model the round and evidence records declare; they used to run on the runner’s default (haiku) while the records named the routed model. An operator’s--modelstill wins and the routing says so withmodel_source: operatorand the routed model beside it. Every evidence record and per-case row now carriesmodel_run, the model passed to the client. (#721) -
A hook you add beside the harness’s own in
~/.claude/settings.jsonor Codex’shooks.jsonis no longer reported byharness syncas a changed owned field, is not drift forharness doctor, and stays in place atharness uninstall, which removes only the harness’s entries. Before, one such hook froze the harness’s entries for that event. (#725) -
A native evidence record now pairs every observation with the case it belongs to. The record is written with sorted keys, which reordered
casesalphabetically and leftobservationsin the order the cases ran, so pairing by position put most observations against the wrong case. Each case observation now opens with its case name, the list follows the record’s case order with one entry per case, and a record rebuilt from an older durable log with--from-progressgains the same pairing. (#728) -
The
role-confinementnative case now has roles attempt the writes step 4 of the qualification procedure names. An isolated read-onlygathereris told to create a file in its workspace, and the planner, run with a valid--artifact, is told to write inside the workspace and above it. What stopped each write is read from the worker run’s own event stream, its init event’s tool set or the refused call, and a file that lands fails the case; a role that held a write tool and never used it is unverified. The--artifactpath refusal it read before is kept, reported as the command-line check it is. (#729) -
The
hook-compositionnative case now reads step 4 from turns rather than from the merged settings table. One headless turn writes two files with the client’s Write tool in an untrusted repository that has a## Gateblock, and the case reads the user-owned hook’s own log line for each file and the stop gate’s loggeduntrustedverdict for that session. Thegrade-bashdeny under an acknowledged bypass is read from the deny turn’s permission denials and attributed to the hook by its decision-log row. A turn that never uses the file tool, or writes only one file, is unverified. (#730) -
The
stance-switchnative case now switches a communication stance beside the delegation one: it cyclesvoicefromscannabletoanswer-card, reads the resolved voice text and output style, and asks the same comparison prompt under each, passing only when the reply carries a table underscannableand none underanswer-card. Thecustom-stancecase now also applies a project override in a disposable repository and reads that a turn inside it follows the override, a turn outside it follows the global selection, and the global link does not move. Anoffturn that never attempts the spawn is nowunverifiedrather than failed. (#731) -
The
spawn-confinementnative case now observes all of step 9 of the qualification procedure, and step 9 now states the claim it can prove. A review layer’s spawn carrying the framework’s own spawn text and naming no role must be refused, and the refusal, tied to its decision-log row by the brief’s fingerprint, must name the framework, the layer andharness role run <role>. Recognition is lexical, so a brief the model rewrites in its own words is run and recorded as the claim’s observed limit rather than passed or failed (#739). The same layer then runs throughharness role run, and a run that leaves no isolated worker state or returns no findings fails the case. Two ordinary spawns, one whose brief mentions review, a diff and findings in passing and one that edits a file under the framework’s input roots, must still run unrefused. (#732)
[0.12.0] — 2026-09-22
Added
-
Subagent rows in the usage ledger now record whether the return carried a path a reader can open and whether it stayed inside the word cap its brief stated.
return_pathis"resolvable","unresolvable"or"none"— a return that named no path carries none, which is a fact about the return and not a failure — over paths written in a fence, in backticks, or bare in prose with a path’s own shape, sopass/failand2026/09/22stay prose and a URL names nobody’s file here.return_over_budgetcompares the return’s words against the caprule-detectorsreads from the brief, or the defaultbrief-guardappends to a brief that states none; an empty brief and a spawn whose requested type carries its cap in its own definition are left unmeasured, and a result the scan kept only the head of recordsreturn_measured: "truncated"rather than a figure taken over part of it. Both are a string match and anos.path.existstaken when the row is written, never a model judgment, so the ledger holds labelled input rather than an opinion.harness usage --by rolereports the share of returns that named a path whose path resolved, and the share measured against a cap that ran past it, printing-where neither applies. Codex joins no return to a subagent row, so its rows recordnulland its capabilities file names the gap. (#416) -
harness remote-controlsupervises each host through Claude Code’s ten-minute give-up:healkeeps the bridge pointer naming the live environment, stops a host with oneSIGTERMat nine minutes unreachable so launchd’s relaunch takes the preserving path, and recreates the session worktrees a give-up already deleted.statuslists sessions left active but disconnected on this Mac’s environments and prints the manual reattach command for each — it never runs one, because a--session-idhost registers the lost environment a second time and the client then routes new chats to it.doctorreports each host’s process, environment, pointer freshness and disconnected sessions. Workspace trust is now read for the exact folder, as Claude Code reads it, and an untrusted folder failsinstallwith the fix instead of looping launchd (#483). -
harness remote-control install|status|uninstallkeeps one Claude Code Remote Control server perremote_control.foldersentry running under launchd on macOS, so a new session can be started from the mobile app with no terminal open. A folder whose workspace trust was never accepted is reported and skipped, never trusted on the user’s behalf. -
harness usage --by prefixreports whether each session held its cached prefix: the cache-miss ratiocache_write / (cache_read + cache_write)from the ledger rows it already has, with each subagent row’s counts subtracted from its parent because the runtime folds them in, and the slice at which the ratio rose most sharply. A session whose subagent rows are missing, a row with no cache fields, and a runtime that never reports cache writes all reportunknownrather than a figure. It measures and does not enforce; Codex exports no per-turn cache figures, which its capabilities file now records (#415). -
A
/close-outworkflow ends a finished session in one invocation: it sweeps for work still open — dirty checkouts,harness worktree audit, this session’s pull requests and their checks, running background work, parked decisions — delegates the merge to/landand the progress file to/handoff, batches the follow-ups it found for filing on an explicit go-ahead, messages the sessions whose work this one changed where the client can reach them, and archives only when the invocation asked it to. It never clears or compacts first, because archiving ends the session and both only spend context the close-out is still using (#426). -
Qualification evidence is written per case as the case finishes, so a round killed part-way costs one case rather than the whole round.
scripts/native_acceptance.pyappends each finished case to a durable log outside the checkout,--progressnames that log and--from-progressrebuilds a record from it without launching a client. The evidence schema already unions cases across several records per client and already rejects a linkedfailedorunverified, so a partial record stays a valid input and neither behaviour changes (#339). -
The landing copy now names the decision log: the
Live usage feedfeature inproduct.jsonand the matching README line say that a decision log on this machine records what a hook decided and what settled it, and that onlyharness usagereads it. A newlanding-copycheck enforces the rule that produced the gap: a pull request touchingbin/,lib/,adapters/,primitives/orpolicy/must also changeproduct.json, or carry aLanding copy:line in its body saying why no copy change is needed. A docs-only, tests-only or CI-only pull request never fires it (#390). -
audit --livereports a parent only GitHub records asrun refreshrather thanrun apply, so a sub-issue added after its mapping cannot be detached by the next projection;refreshadopts it (#388). -
The
issue-ownershipcheck now refuses a pull request whose delivery issue has no BMad ID in the issue map, andscripts/bmad_issue_sync.py newfiles an issue and reserves its ID in one step (#378). -
scripts/bmad_issue_sync.py audit --livecompares the committed issue map with GitHub, read-only: title and open/closed drift, a missing Planning block, and an accepted issue that has no BMad ID.refreshcopies GitHub’s title and state back into the map, and abmad traceabilityworkflow runs the audit daily and on issue events (#218). -
Release upkeep is written down and checked.
AGENTS.mdanddocs/releasing.mdnow say when a release is proposed (cut by milestone, or when a user-visible unreleased change is seven days old, with a regression fix released at once as a patch), how it is numbered by what changed rather than by changelog section, and the exactgh apicommands that close the released milestone and open the next. The seven release surfaces now mark the reference site and the personal-site card as verify only, because each repository deploys itself. A newscripts/sync_about.pycompares the GitHub About panel withproduct.json:--checknames every differing field and exits non-zero,--applywrites them throughgh repo edit, and topics compare as a set.--applystays a local step needing the owner’s approval, because editing repository settings needs administration access and no such scope exists for a workflow’sGITHUB_TOKEN.scripts/release_preflight.pyruns that comparison and looks up everyon_the_wayentry that names an issue, blocking the release with “promote or remove” when one has closed. Both checks sit behind onegh auth statusprobe: without an authenticatedgh, which is the case in the tag workflow, they are skipped with a named warning rather than silently, and aghfailure after a good probe blocks the release. -
A local, append-only decision log,
~/.local/state/agent-harness/decisions.jsonl, beside the usage ledger: one record each time a hook makes a judgment, and a second record when the session settles it.grade-bashrecords the permission answer it gave a command and joinsranwhen the command’s PostToolUse arrives, ornot_runwhen the session ends without one — not “denied”, because a refusal, an interrupt and a crash look the same from a hook.stop-gaterecordsblocked,releasedorskippedwith the gate’s own result;tier-agent-spawnsrecords the band an unnamed spawn was routed to,brief-guardwhat it wrote into the brief, andevasion_denya refused re-spawn. A row holds the text the hook judged, capped at 2 KiB, with the hash taken over the uncapped text; it holds no tool output and no assistant prose. Nothing is exported, nothing is model-visible, and a write that fails is counted and swallowed rather than allowed to change a decision.telemetry.decisions: falseturns it off entirely. Read it withharness usage --by decision, which prints counts, the outcomes seen per point and the unlabelled share. Band routing happens on Claude Code alone, so Codex writes notier-agent-spawnsrow and its capabilities file names the gap. -
Subagent rows in the usage ledger now carry
budget_output_tokensandbudget_tool_calls, the soft budget the role they ran as carries, so an overrun is a subtraction on one row rather than a join against whatever the cost table says today. A role nothing prices recordsnull, because a zero would say the spawn was budgeted nothing. -
Plan mode now investigates at the permission posture you selected instead of below it. Under
bypassorautoin Claude Code, the PreToolUse coordinator approves the commands native plan mode prompts on — a script run, apython3 -c, a scratch redirect, a test run, anything graded 0 or 1 — and asks about grade 2, because a push or a mutating API call is execution rather than planning. Grade 3, the confirm marker,manual,inheritand Codex are all unchanged, and a stricter autonomy stance still wins. A new config key,plan_allow_tools, lists tool-name globs (such asmcp__notes__read_*) approved in plan mode under the same posture gate; it is empty by default, because a hook payload carries no read-only hint for an MCP tool and nothing is inferred. -
product.jsonnow holds the landing copy as validated data: aheroof title, subtitle and proof line, sixcapabilitiesgroups of a pitch and three to six features each with a repository relativedocpath, and anon_the_waylist of at most five items, each naming an issue, a client the compatibility catalog callsplanned, or a document. The hero title is the existingheadline, whichgithub_descriptionalready leads with, so the page, the README and the GitHub About description share one source.tests/test_release.pyfails when adocpath is missing, a group or feature count leaves its range, a feature line runs past 170 characters, any string carries an em dash, or the README and the data disagree. -
Every usage row names the
harness_versionthat wrote it, read from the sameVERSIONfileharness --versionprints, so a change in spend can be read against a release. A rescanned row carriesnullrather than today’s version, and a role-run worker’s row carries the version stamped into itsstatus.jsonwhen the run started. -
A session row records the
effortthat covered the most output tokens and aneffort_sourcenaming where it was read:transcriptfor Claude Code, which writesefforton every assistant record, andturn_contextfor Codex. Effort changes mid-session — 14 of 112 Claude Code transcripts and 4 of 44 Codex rollouts on one machine — so the row weighs it by output rather than taking the first value seen, and recordsnullwhen the transcript names none. -
A session row carries per-day slices in
days: four token figures and a turn count per UTC date, cut from the same deduplicated map the row’s totals are summed over and dropped whole if they do not add up to it.harness usage --by daysums the slices when a row has them and falls back to its end date when it does not, and--daysthen windows on the slice date, so a session that ran for a fortnight contributes only its in-window days instead of landing on the day it ended. Five such sessions were 68% of all output tokens on the machine measured. -
harness usage --by stance --stance <dimension>groups tokens by that dimension’s variant. Rows with no recorded stance, and rows a rescan stamped, are counted under(unknown)rather than dropped.--rules --by stancekeeps the hit report unchanged, and--by stancewith neither is refused with a usage error rather than guessed at. -
harness usagereports dollars.policy/prices.jsonlists USD per million tokens for input, output, cache read and cache write per model id, each entry carrying theas_ofdate it was read and the provider pricing page it was read from; a model whose price could not be confirmed from a primary source is absent rather than guessed. Ids resolve by longest prefix after normalisation, so a dated, Bedrock-style or[1m]id reaches its family. Every token grouping gains ausdcolumn and anunpricedfooter,--by rolegains p50 and p75 dollars, and a row with an unknown model, two models or partial tokens is unpriced rather than $0. Apricesblock inconfig.jsonmerges over the file per model id. Checked against a recorded Claude Code session whose runtime reportedtotal_cost_usd = 0.60097775: the table reproduces it exactly. -
A session row records a per-model token breakdown in
by_model, checked against its own totals before it is written and dropped whole if it disagrees: cut from the deduplicated message map for Claude Code, and from the snapshot deltas under eachturn_context.modelfor Codex. A row that carries one is priced model by model, which is the only way the sessions that switched model — the largest ones — can be priced at all. On a 60-day rescan, unpriced runs went from 26 of 143 to 0, and the share of output tokens that was unpriced from 73.7% to 0%. -
A usage row records the cache-write tier split Claude Code reports —
cache_write_5mandcache_write_1hbeside thecache_writetotal — because Anthropic prices a 5-minute write at 1.25x base input and a 1-hour write at 2x. The keys are additive, so an existing row stays readable and is charged whole at the 5-minute rate. -
harness doctornames the newestas_ofin the price table and warns when it is over 90 days old, since prices go stale silently while the report keeps printing dollars. -
Ledger rows can be exported to any OTLP/HTTP endpoint, off by default. A
telemetryblock inconfig.jsonturns it on; with it off no network code runs and the hook behaves as before. Each row becomes one OTLP/JSON log record onPOST <endpoint>/v1/logs, sent from the detachedSessionEndworker after the row is already in the ledger: one attempt, a two-second timeout, no retry, and a failure recorded inusage.errors.jsonlrather than in the session’s exit status. The body is the row; the attributes are its flat scalars plus a stableharness.row_key, the harness version and oneharness.<dimension>per recorded stance. -
Request headers are read from a named environment variable or a file outside every git work tree that no other user can read; a header value written into
config.jsonis refused by name, and no value is ever printed, logged or written to an error record — a failure names the endpoint’s scheme and host only. -
harness usage export --since <date> [--until] [--dry-run]replays a window of rows in batches, prints what was sent and what failed, and exits non-zero if any batch failed. Delivery is at-least-once, so docs/telemetry.md gives the de-duplication query onharness.row_keyand states the model: the ledger is the record, a backend is a rebuildable copy, and replay is the recovery path when a backend’s retention expires or it is rebuilt. -
harness doctornames the export mode, the endpoint’s scheme and host, and the names — never the values — of the headers it resolved. -
A
/landworkflow picks up where/buildstops. It verifies the required checks — and the issue-ownership check where a repository runs one — on the head that will actually merge, squash-merges with the remote branch deleted, fast-forwards the shared checkout, removes the managed worktree, deletes the local branch withgit branch -d, audits for stale checkouts, then reads the repository’s own release rule and either says no release is due or posts a release card for approval. It never forces a removal, never usesgit branch -D, and stops on dirty or unmerged state with the reason; merging stays approval-gated and it never tags or deploys. Projections for both runtimes are generated from the shared source as usual. -
"native": truein thetelemetryblock makesharness syncturn on each runtime’s own OpenTelemetry export to the same endpoint, off by default. Claude Code gets the telemetry switch, both OTLP exporters,http/protobuf, the endpoint and anOTEL_RESOURCE_ATTRIBUTEScarryingharness.versionand oneharness.<dimension>per resolved stance, plus anotelHeadersHelperpointing at a harness-owned script that reads the configured header source at run time — so no header value is written into a settings file. Codex gets[otel]with bothexporterand an explicitmetrics_exporter, because its default metrics sink drops token, cost, tool and API metrics client-side; it is given no header, since[otel]takes a literal header map, and docs/telemetry.md states that gap rather than papering over it. -
Ownership is per variable: a variable you set in
envis never read, changed or removed, a managed key already holding a value the harness did not write is reported and left alone, and turning the key off restores what each key held before. Labels are frozen at sync time, so a stance switched without a re-sync mislabels native data until the next one — the ledger row stays authoritative.harness doctorreports the state, the endpoint’s host, whether the labels are current, and that both runtimes attach their own user and organization identifiers. -
docs/telemetry.md ends with a reference recipe for one backend that was set up and measured end to end — the ClickStack all-in-one image — as a worked example of “any OTLP/HTTP endpoint” rather than a requirement: the run command with its three persistent volumes, the two manual steps before a single record is accepted (create the first user, then send the ingestion key as a bare
authorizationheader), the 30-day TTL every table ships with and theALTER TABLE … MODIFY TTLthat raises it, and why Codex cannot reach a backend that needs a header. It contains no command that creates an account, stores a password or removes a container or a volume, and it names the licence of every part of the image. -
A dashboard definition ships beside it as docs/telemetry/clickstack-dashboard-native-cost.json: ten tiles of this repository’s own SQL over the standard OpenTelemetry tables, reading the native Claude Code cost and token metrics down to agent, model, effort and stance variant. The instance-specific connection id is a placeholder, with the one-line lookup beside it.
-
scripts/cost_bench.py replayruns a pinned task set headlessly against two profiles — a signed-in, otherwise empty Claude Code profile and the installed harness — on one shared command line and a scrubbed environment, and scores each run with a check the agent never sees.benchmarks/tasks.jsonpins the tasks,benchmarks/oracles/holds the held-back checks, and--verify-tasksproves every check and refuses a fixture whose own gate is red or whose solving commit is still reachable. It calls a model and spends real usage, so it is run by hand on a release candidate and never in CI, and no result is published with this release: the runner ships, the number does not. Reading and limits:docs/benchmarks.md. -
scripts/cost_bench.py staticmeasures what the harness adds to every Claude Code session against a bare one: files, lines, characters, an estimated token count and its price per model frompolicy/prices.json.benchmarks/static.jsonholds the figure for the last release and CI fails when the estimate grows more than 5% over it without an entry inbenchmarks/allow.json. No model is called. Reading and limits:docs/benchmarks.md. -
An exported ledger row carries
harness.usdandharness.price_as_of, so a dashboard reads dollars instead of re-implementing the price table in its own query language. The figure is a list-price API equivalent fixed at export time, computed by the codeharness usageprices with: the rates, the id normalisation and the session/subagent join moved topolicy/hooks/pricing.py, which the CLI and the standalone export hook each load rather than either one holding a second copy. A session row’s figure already includes its subagents, exactly as the report totals them. An unpriced row carries neither attribute — never a zero — and a missing price file or a malformedpricesoverride costs an export its dollars and nothing else. -
Qualification freezes on a release branch instead of on
main.compatibility/freeze.jsonnames the frozen commit and branch, and a newharness freezereports drift between that commit and a given ref over the runtime source paths, exits non-zero on drift, and refuses a merge that would change those paths on the frozen branch.docs/releasing.mdgains the freeze procedure and the triage rule that a round runs to completion and collects defects rather than fixing one mid-round, because any source-path change invalidates every evidence record for the round (#332). -
Two deterministic pre-qualification checks that spend no model turns. A documentation-link check resolves every relative path and heading anchor across the repository’s Markdown, and a credential probe names the variable a client would authenticate with or fails immediately with the reason, instead of the 300-second hang a session-credentials-only home used to produce. Both are additive and neither writes to
compatibility/evidence/or appears in a catalog record, because a green deterministic check is not native qualification (#401).
Changed
-
The credential probe’s docstring records why its green line is a fixed string that never names the variable it found: a clean CodeQL scan with no dismissals was judged worth more than the name on stdout, and a caller that needs it calls
reachabledirectly (#409). -
telemetry.nativetakestrue,falseor a list of runtime names, so a collector that authenticates can be fed natively from Claude Code while Codex, which takes header values only as literals in its configuration file, is left alone.truekeeps its meaning, an unknown name stopssync, anddocs/telemetry.mdsays which runtime can reach an authenticated endpoint (#405). -
The README’s install command clones the
stablebranch, so a new install starts from the latest release instead of the development trunk. -
The README’s first screen is the headline, a terminal capture of
harness sync --dry-runon a fresh home, the description and the six capability groups, one linked line per feature, plus the “On the way” list. Release status and the generated compatibility block now follow the install section, so a first-time reader meets what the harness does before which clients are qualified. -
harness usage --by rolemarks a role with fewer than 30 runsn<30in a newsamplecolumn. A p90 over eight runs is the second-largest of eight, and the budget re-seeding procedure indocs/usage.mdnow says not to re-seed from a marked row. -
The fan-out warning in docs/usage.md now carries what was measured against it. Across 137 sessions on one machine the cache hit rate was 97.0%, 97.2%, 97.3% and 97.1% at 0, 1–6, 7–50 and 51-or-more subagents, so the falling hit rate it describes is a thing to check in your own data rather than an expectation.
Fixed
-
The test suite no longer writes the harness into a real profile.
claude_dir()honoursCLAUDE_CONFIG_DIR, so a suite run in a shell that had it set installed links and rendered files into whatever profile it named instead of the run’s own temporary home. Every test now clears it through one shared isolation helper, and the sandbox fence that hid the leak is no longer what keeps a caller’s profile intact. -
The usage feed says
spend unknownonce and names the agent it could not read, feeds a cumulative line for every round of an agent resumed with a follow-up message rather than only its first, and states once per session what its token figure measures so it cannot be read as the task notification’ssubagent_tokens(#306). -
harness syncinstalls the Claude Code output style from thevoicestance instead of unconditionally:scannableinstallsScannable,answer-cardandoffinstall none, a style you chose yourself survives, and a harness-installed style is removed when the variant changes. Ownership comes from the reconciliation journal rather than the style’s name, so a style you picked before installing is left alone even when the harness ships one by that name. The Codex projection is unchanged;scannablewas already the only variant it gave presentation material to, so the two runtimes now mean the same thing by each variant (#394). -
harness usageno longer prints the partial-totals warning above a USD report whose own footer already counts those runs as unpriced; a report with no unpriced run still warns that a column is short (#407). -
.agent-harness/evidence/,task.jsonandsync.lockare ignored repository-wide, so captured replay transcripts can never be staged or reach the lint, and the stop gate no longer goes red in a checkout whose own change is clean; a test pins the rules (#432). -
Three defects in the native acceptance runner, recorded during 0.11.0 qualification and none of them in the harness runtime.
orchestrator_text()now reads a session’s transcript whether or not a subagent directory was written, so a session that spawned none no longer returns an empty string and lets acost-postureassertion pass vacuously. The runner’s cited qualification runbook is written rather than a dead link. The three AWS session variables — the key id, the secret key and the session token — now reach the client, so a container holding only session credentials can authenticate. The bypass classification apermission-controlsdriver needs — telling a model’s own refusal apart from a policy block by reading the recorded denials and the permission mode — is added and tested, but no such driver exists in the runner, so nothing calls it yet (#309). -
harness worktree removefinishes the cleanup after a squash merge. The quality gate has to run before a push and writes__pycache__, which used to leave the worktree unremovable and reported as dirty byharness worktree audit; removal no longer counts the regenerable caches it knows (__pycache__,.pytest_cache,.mypy_cache,.ruff_cache,node_modules,.astro, plus a top-level directory named with--also-clear NAME) as work, git deletes them with the checkout, and audit reports a checkout holding only those as clean. Modified, untracked and other ignored entries still refuse, naming the first few. With--mergedthe local branch is deleted too, but only onceghreports a merged pull request whose head commit is the branch tip: after a squash the branch’s commits never reach the default branch, so ancestry cannot answer andgit branch -drefuses work that did land. A missing or failinggh, no merged pull request from this repository, a tip the merged head does not match, or the default branch refuses with the reason and deletes nothing./landnow hands both steps to one command. -
The documented de-duplication query runs as written. Every exported record now carries
harness.exported_at, the export time as a fixed-width RFC 3339 UTC string, and the example indocs/telemetry.mdorders on it instead ofObservedTimestamp— a column the OpenTelemetry ClickHouse exporter’sotel_logstable does not have, since the observed time is dropped on ingest andTimestampis the row’s own end time, identical across replays. Attributes land in that table as aMap(String, String), so the stamp is fixed width for lexical order to equal time order, and the example castsharness.usdwithtoFloat64OrNullbefore summing it. A test checks the example names no column outside the real schema. -
Each stance reaches a backend once, as
harness.<dimension>. The exported body no longer carries thestancesmap, which a backend that parses a JSON body flattened into a second dotted copy of every stance beside the attributes; every other field still travels in the body. -
A client launched under a substituted
HOMEno longer raises the macOS “A keychain cannot be found” dialog. The acceptance runner already gave its disposable homes a keychain, but two other launches did not: every role worker runs its client in a private home that had none, andharness doctorranclaude doctorin whateverHOMEit was given, including a throwaway one an agent built to test a config. A role worker’s home now carries its own throwaway keychain, and a worker whose keychain cannot be created fails instead of launching;harness doctorskips the client’s doctor, and says so, whenHOMEhas no default keychain.harness keychain <home>is the same guard for a home you build by hand. Other hosts are unchanged. -
The
gathererrole no longer declares web tools its only execution path cannot give it. The role listedWebFetchandWebSearch, the spawn guard refuses a nativegathererin favour ofharness role run gatherer, and that isolated worker is launched withRead,GrepandGlobunder a read-only sandbox with hosted search disabled — so a web dimension of/researchhad nowhere to run. The confinement stays: a worker that can both read a workspace and fetch can carry what it read back out, and a fetched page is untrusted input inside a confined process. Instead the declaration now matches the launch, the role says it is offline and that online evidence arrives as files granted with--read-dir, the refusal that points atharness role runadds where a web dimension goes instead, and/researchroutes by where the evidence lives — files and repositories to the isolated worker, the live web to an in-session band worker. A test asserts the command line agathererworker is actually launched with. -
transcript-hygiene/brief-without-capis nowtranscript-hygiene/model-wrote-no-cap, because that is what it always measured. A transcript records anAgentcall as the model wrote it, and aPreToolUsehook’supdatedInputis written to a separateattachmentline the scan never reads — sobrief-guardcapping a brief could not move the number, and the rate was unchanged before, during and after the hook shipped. The detector’s behaviour is unchanged and the rename makespromote?on it mean something: the orchestrator writes no bounds and the hook is carrying the rule.rule-detectors.RENAMEDnames the successor andusage --rulesfolds it as it reads — by rule, by repo and by stance — so a row written under the old id reports under the new one, with no rewrite of the ledger file and no split in the series. -
A Codex subagent thread is recorded as a
kind: "subagent"row joined to the thread that spawned it, with its depth, nickname, model, effort and tool-call count, instead of as a session of its own. On the machine this was measured on, 307 of 438 rollouts were subagent threads and every one of them was a session row. A thread that inherited its parent’s history writes the parent’ssession_metafurther down its own file; only the first one is read now, which is what 36 of those 307 turned on.harness usage --rescanmigrates the misclassified rows, deleting the stale keys and copying the ledger tousage.jsonl.bakfirst. -
The rescan reads
~/.codex/archived_sessions/as well as~/.codex/sessions/. Codex moves a rollout there unchanged, and 96 of the 131 top-level rollouts on that machine lived only in the archive, so most Codex sessions never reached the ledger at all. Codex capture is rescan-driven: whether the runtime’sSessionEndpayload names the rollout file is not established, so the hook now acceptsrollout_pathandsession_pathbesidetranscript_pathanddocs/usage.mdsays the rescan is the path known to work. -
A Codex session whose snapshot carries
total_tokensalone — 85 of 107 top-level Codex Desktop rollouts — is recorded aspartialwith atotaland unknown typed fields, rather than summed as a session that spent nothing. -
harness usagesums Codex subagent rows and still skips Claude Code ones: a Codex thread’s total counts that thread alone. Four of the 21 measurable parent threads report fewer tokens than their own children sum to, which a total including them could not do.--by modelalso reads the singlemodela subagent or worker row names instead of grouping it as unknown.
[0.11.1] — 2026-09-21
Added
- A
stablebranch that always points at the latest release. The release workflow fast-forwards it to the tag’s commit after publishing,scripts/advance_stable.py --checkverifies it, and the branch never moves backward.mainstays the trunk.
Changed
- The
builderrole’s report closes two gaps a downstream soak found. A hand-edited fixture, golden file or pinned value must now name the generator or the command that produced it — “hand-typed, copied from run X” answers it, silence does not — and where a generator exists the builder regenerates instead of hand-editing. The gate’s result is read from the test command’s own exit status, captured withPIPESTATUS,pipestatusor no pipe, rather than from whatevertailreturned. The fixed report gains one item for the edited fixtures and what produced them. - Qualify the Claude Code and Codex CLIs on macOS and Linux for this source with version-pinned native evidence across all eleven acceptance cases, and record the limitations those runs established in the compatibility catalog.
Fixed
- The Review Card’s diagram is a plain-text drawing in a
textfence. Theplan-authoringskill, its template and example, and theplannerrole defaulted to a mermaidflowchart, which the plan-mode pane and the chat sidebar show as raw source — so the card’s one diagram was unreadable where the card is reviewed. New or changed nodes carry a*; mermaid stays for the addendum and for docs read on GitHub. - The
delegation: offstance said a hook asks before any spawn, where the lifecycle denies the spawn outright and never reaches that hook. The stance now says a spawn under it is denied and that changing the selected stance is the way to delegate, and it no longer names a hook — the name it used,tier-spawns, was not the file doing the refusing either. - The spawn guard no longer refuses a constrained role only by the name a spawn chose. Refusing a
native
reviewerspawn moved the work rather than stopping it: the client re-issued the same brief as an unnamed subagent and it ran unconfined. A refusal is now remembered for the session, and a later spawn that names no constrained role but carries the same brief — normalised, or a near-identical rewording — is refused with the sameharness role runinstruction and told that dropping the role name changed nothing. Independently, a brief whose own first line readsharness-role: <role>may only run as that role’s isolated worker, whateversubagent_typethe spawn names or omits. The BMad review layers now carry that line. Session state that cannot be read or written leaves the guard exactly as it was, anddelegation: offis untouched.
[0.11.0] — 2026-09-21
Added
- Add the
designerrole: one pass of visual design work toward a locked target, validated and captured, never self-scored. It declares thefrontierclass, which is the only way a spawn reaches the strongest model now that the hook refuses it by request. The design loop hands it the build and fix steps;design-judgestill scores from a fresh context. - A usage feed tells the orchestrator what it is spending while the session runs: a turn line on
UserPromptSubmit, a line for each subagent as it returns, a line at the next prompt for every background spawn that finished meanwhile, and a note when more agents are running than the variant’smax_parallel. A subagent’s figure is summed from its own transcript, because a tool response reports only that agent’s last response. The behaviour isturn_feed,nudge_atandmax_parallelin the activecostvariant;offinjects nothing and writes nothing. Per-session state is an append-only journal plus aflock-guarded reader file, because these hooks run concurrently and nothing slow runs under that lock; a figure that could not be summed inside the hook’s budget is reported as(partial)orspend unknown, never as zero. Stale files are swept after a fortnight andharness uninstallremoves them. Codex raises none of the three events and declares the feed uncovered. harness tiers checkcompares the Codex class table with the model catalog Codex fetches from its provider, offline, and fails on a mapped model that is gone, superseded or ranked out of order. A missing catalog reports unverified, not a pass.tiers.<runtime>.<class>in the configuration remaps a capability class for every role that names it, without a harness release.- The usage log records one row per subagent and one per
harness role runworker beside the session row, each naming itskind, agent type, model, effort, token counts, tool calls and spawn depth. Rows are upserted by(session_id, runtime, kind, agent_id)and hold counts only: no prompt text and no command text. - Each
costvariant carries a JSON sidecar beside its.mdholding its switches and its model class, effort and soft budgets per role and per band. A variant resolves over itsextendschain, a variant with no sidecar resolves tobalanced’s, and an unknown key is a warning rather than an error so a later release cannot break a variant you wrote. posture: fixedin a role’s frontmatter, set onreviewer,spec-reviewer,design-judgeandlog-compressor: a cost variant may budget the role but never change its class or effort.harness stances --jsoncarries the resolved cost table — switches, rows with base and scaled budgets, default band, theextendschain with each sidecar’s path, and warnings. Lint validates shipped sidecars against the schema and against their own prose.- Three band worker roles,
worker-a,worker-bandworker-c, carrying the A/B/C bands’ class and effort into a native spawn. Their descriptions hold the band rule, so an orchestrator chooses a band by spawning one of them by name. They name no tool list, so a rerouted spawn keeps every tool it had asgeneral-purpose, MCP tools included; an adapter role entry may now carrydisallowed_toolsinstead, which is how they give back the one tool a role withdelegation: nonemust not hold. harness usage --by rolereports, per agent type, the number of runs and the p50, p75 and p90 of output tokens and of tool calls over the window — the distribution a per-role budget has to be set against. A run whose runtime reported no counts is named, never averaged in as a zero.- Every brief states the spend the cost variant expects of it: the row’s output tokens and tool calls in one sentence, soft — finish if close, otherwise return what you have — because a subagent cannot see the variant that priced it. A spawn that named a role is priced by that role and one that named none by the band worker it is about to be routed to, computed by the same function that routes it — and only on a runtime that reroutes, so a spawn Codex will run as written is priced by its role or by nothing. A brief that already prices itself, an unbudgeted role and a table that will not build are all left exactly as before.
Changed
- Claude Code’s native agent definitions follow the resolved cost variant. A role the selected
posture does not move keeps its symlink to the committed projection, so a default install is
exactly what earlier releases wrote; a role it does move is rendered and written as a managed
file, which is how a variant’s class and effort finally reach a native agent. The class its row
names resolves through the adapter’s
tierstable. Precedence is the role’s own tier and the adapter’s effort, then the variant’s row, thenrole_bindings.<runtime>.<role>, which still wins; aposture: fixedrole takes neither cell. Effort and model are therefore sync-scoped: a sessionHARNESS_STANCE_COSTdoes not move them until the next sync. Roles move between link and file in both directions as the posture changes, and a definition you edited or a link you redirected is preserved and reported, never replaced.sync --dry-runnames each role whose rendering has moved, with its class, model and effort. - Stances resolve in one place for the dispatcher and every policy hook alike: built-in defaults,
the user configuration, the file
HARNESS_PROJECT_CONFIGnames, thenHARNESS_STANCE_*. The hooks therefore honourHARNESS_HOMEand a project configuration, which they ignored before, so a disposable home or a per-repository selection now reaches the spawn, brief, grading and usage hooks rather than only the CLI; both are environment variables the user sets, at the same trust level as theHARNESS_STANCE_*the hooks already honoured. The grading hook is the exception that fails closed: a stance it cannot resolve is graded under the strictest variant, named as unresolved in the prompt. - A spawn that names no agent definition, or names
general-purpose, is rewritten to the cost variant’s default band worker and runs on that band’s class — the only way the posture’s effort reaches it, because theAgenttool has no effort input. A model the caller named is kept, a request for the top class is refused and the band’s class applies in its place, and a variant with nodefault_band, an unreadable table, a machine whose worker definitions are not installed and a session whose agent registry predates them all leave the spawn exactly as the previous release did. A repository that ships its own.claude/agents/worker-<band>.mdis never routed to, because a project definition outranks the user’s. The cost table is read only for a spawn that named nothing, so naming a role costs nothing. - Subagent usage rows carry
requested_typeand setreroutedwhen the type the parent recorded differs from the one the subagent ran as, joined on the tool use id. The reroute is measured from the transcript rather than reported by the hook that made it. - The spawn hook reads its model ladder from the adapter’s
bindings.jsontierstable instead of a list written in the hook; a table it cannot read leaves the spawn as written and says so. - The spawn hook says so when the session’s model is not on its ladder, instead of leaving the subagent on the session model without a word.
- Shared roles name a provider-neutral capability class (
tier:—frontier,strong,standard,light) and each adapter’sbindings.jsonmaps classes to native models in atierstable.reviewerandplannerrun onstrong,spec-revieweronstandardanddesign-judgeonfrontierinstead of inheriting the session model, so their cost no longer follows whatever the session happens to run. An unmapped class resolves upward or inherits, never downward. - Codex roles gain model tiering: its table maps the four classes to
gpt-6-astra,gpt-5.6-sol,gpt-5.6-terraandgpt-5.6-luna. Codex roles previously inherited the session model. - The spawn hook tiers a planning-framework repository like any other, and refuses the top class
by request: an unnamed spawn asking for it runs on the band it is routed to, or one class below
the session where nothing routes it, and a named agent falls back to its definition. The
session-modelstance is unchanged and remains the opt-out. harness role run, the constrained-role refusal message and the BMad override templates no longer tell the caller to pass the parent session’s model; they name it only where the adapter maps none. Role effort abovehighis rejected.- Native qualification now requires an eleventh case,
cost-posture, so a client cannot be qualified without the cost posture layer having run natively: the roles a variant moves and only those, an unnamed spawn routed to the default band worker at its row’s model and effort, the budget sentence in its brief, the feed andharness usagerows against that budget, a session that predates the workers left alone, and the priced-nothing variant doing none of it. Evidence is scoped to the harness version it records, so 0.9.0 and 0.10.0 records stay valid history. - Qualify the Claude Code and Codex CLIs on macOS and Linux for this source with version-pinned native evidence across all eleven acceptance cases, and record the limitations those runs established in the compatibility catalog.
Fixed
-
Nearly every spawn raised a permission-hook notice about a rewrite that is the ordinary case: the budget sentence
brief-guardappends now carries no notice at all, and the line naming the band an unnamed spawn is routed to is said once a session. The return-bound notice, the refusal of the strongest class by request and the repository-supplied-worker refusal are unchanged, because each reports something the caller asked for being changed or refused. -
An unnamed spawn was rerouted to a band worker whenever the definition existed on disk, which failed every such spawn in a session that was already running when
harness syncinstalled the workers: the tool loads its agent registry once, at process start, and rejected the type. The SessionStart policy now records what each session’s registry held, and a reroute requires the worker to be in that record; without one the spawn keeps the previous one-rung behaviour and the session is told once to start a new one. Only a session’s own start may widen that record: a resume narrows it to what is still on disk and creates none, and any failure to answer the question at all leaves the spawn unrouted rather than raising into a refusal. Records are private to their owner, refreshed while a session is in use, swept after a fortnight, and removed byharness uninstall. -
A session whose transcript carried both a subagent’s sidechain lines and that subagent’s own file counted every delegated token twice. The session’s totals are now taken over one map of message ids that both reads fill, so a message recorded in two places is one message. Agents nested under
subagents/workflows/wf_<id>/, which the Workflow tool writes and a flat walk missed entirely, are counted too and name their workflow. -
Output tokens were undercounted, by a factor of several on a long response. The log took each message id’s usage from the first transcript record carrying it, and the early records of one streamed response carry a partial
output_tokens— 7,126 against the response’s real 40,868 on a measured subagent transcript. Each message id now counts at the largest figure it ever reported, so a reordered or truncated tail cannot lower it either.harness usage --rescancorrects the recorded history. -
Every session total was short by whatever its delegation cost. A subagent’s tokens live in its own transcript, which the log never read, so a session that fanned out reported only the orchestrator’s own spend. Session totals now include their subagents’;
harness usage --rescanbackfills the history, and the token groupings sum session rows alone so nothing is counted twice. -
A managed link that reaches its file through an alias of the checkout, such as
claude/stancesforprimitives/stances, is no longer reported as redirected.harness uninstalland the retirement of a removed link treated the same link as the user’s and left it behind; they now remove it. A link pointed at a different file is still reported and still preserved. -
A named agent asked onto the top class now gets the model its definition names, or the class below when there is none to read. The hook used to remove the request, and the lifecycle coordinator only carries rewrites, so the request reached the spawn unchanged.
-
The lifecycle coordinator relays a hook’s notice on Claude Code instead of dropping it, so a tiered spawn and a session model the ladder does not know are both reported.
-
A role worker whose runner died now reports as
orphanedinstead ofrunningforever. The status record kept nothing that could tell a live run from an abandoned one, so a killed session leftstatus: runningwith no result and no error, andharness role statuscould not separate it from work in flight. A run now records the pid supervising it and that process’s start time, and status reports a worker whose process is gone with no result written as the terminalorphaned, writing that state back intostatus.jsonalone. The start time guards a recycled pid; a record from a release that stored no pid, or a platform that will not report a start time, still reads asrunning. -
harness uninstallnow removes the empty directories the sync created for its own files —~/.claude/rules/harness-stancesand each~/.agents/skills/harness-*— instead of leaving them behind. A directory that still holds anything is kept untouched. -
A stale
harness task save --revisionprints one line on stderr naming the remedy and exits 1, where it raised an uncaughtValueErrorand printed a traceback carrying the checkout path. The guard itself is unchanged: a save against a revision that is no longer current is refused. -
harness doctorreports a client that isnot on PATHrather thannot installed, and says so explicitly when Codex credentials are present with nocodexthe shell can reach. -
A permitted Codex tool call no longer reports
hook: PreToolUse Failed. The PreToolUse envelope carriedpermissionDecision: "allow"on every non-gated call; a client that listsallowas unsupported discards the whole hook output, so every allowed call showed a failure and a real one was indistinguishable. Codex now hears nothing where its own default already allows, andallowis sent only with anupdatedInputrewrite, which that runtime applies under no other decision. Denials, the ask-to-deny narrowing and Claude Code’s envelope are unchanged. -
An isolated role worker now follows the selected cost variant.
harness role runbound a role from itstier:alone, so underfrugalagathererworker ran on the role’s own class while the same sync in the same home rendered that role one class lower — and the constrained roles are denied as native spawns, so neither the posture nor the soft budget ever reached the roles that carry measured budgets. A worker now resolves its row through the same function and the same precedence the sync path renders a definition with — role defaults, the variant’s row (class only under a tiereddelegation, never for aposture: fixedrole),role_bindings, then--model— on both runtimes and through the whole stance ladder, so a session-scopedHARNESS_STANCE_COSTreaches it. Its brief ends with the sameExpected spendsentence a native brief gets, from one function shared with the brief guard, unless the row prices nothing or the brief already states a budget;status.jsonrecords the variant, the resolved class, where model and effort each came from, and the figures appended. A variant with no row for the role, or a table that will not build, leaves the worker exactly as it was. -
The
autopermission posture now gives Codex the automatic approval review it promises. Sync wroteapproval_reviewer, and Codex names the fieldapprovals_reviewer: codex-cli 0.154.0-alpha.6.2, 0.155.0-alpha.9, 0.155.1 and 0.156.0-alpha.9 all reject the old spelling under--strict-configand drop it in silence otherwise, so the posture resolved in the client as review by the user with no warning. Sync now asks the installed client which name it accepts — from its own emitted protocol schema, or a--strict-configprobe in a throwaway configuration home, neither of which starts a model turn — writes that one, and takes the stale spelling back out. Both spellings are harness-owned, so a key the harness wrote is removed or restored on re-sync and uninstall while a key of the same name that you set yourself is left alone. With no client installed, the name the newest supported version accepts is written; a client that accepts neither gets no reviewer key, a sync notice and aharness doctorfinding. -
The usage feed reports a finished subagent’s actual spend instead of
spend unknown. ASubagentStopsummed the agent’s transcript the instant it fired, and at that instant the transcript can hold only theuserandattachmentrecords the parent wrote into it — so the stop was journalled with null totals and the reporter printed them, while replaying the same payload a moment later yielded 297. A stop that carries no figure, or one read out of a response still being written, is now summed again on the line that names it: before the lock, with the bounded settle wait, inside one wall-clock budget shared by every agent that event reports.spend unknownnow means a transcript that is not there; a transcript that is there and holds no response yet saysspend not yet recorded, and the figure it gains later raises the session totals without the agent being named a second time. -
The usage feed’s line for a synchronous subagent return no longer stops short of that agent’s last response. Claude Code writes one API response as several records, and the return could fire between a partial streaming count and the record that ends the response — 143 output tokens reported live for an agent a later scan put at 278. The return now waits a bounded moment (at most a second, over the transcript’s tail) for the response to end, says
(so far)when it never does, and raises the session totals from the settled figure the journal brings afterwards without naming the agent a second time. -
A usage row names a subagent’s model one way. A routed spawn’s row carried the alias the spawn hook asked for and a directly spawned agent’s the full id its transcript records, so one model appeared under two names. A subagent row now records what its own transcript reports — the most frequent model across its assistant records — and falls back to the requested alias only when it recorded none;
harness usage --rescannormalises rows already on file. Worker rows still record what the worker reported, which is the only thing that knows. -
The native acceptance runner no longer raises a macOS keychain dialog on every client turn. macOS resolves the default keychain under
HOME, a disposable home had none, and a client that stores an item then prompts “A keychain cannot be found” — once per launch across a whole matrix, with a destructive Reset To Defaults button. Each disposable home now carries its own throwaway keychain at the default path, so the store succeeds silently and never touches the operator’s login keychain; a home whose keychain cannot be created reports the caseunverifiedinstead of launching a client. Other hosts are unchanged. The test suite raised the same dialog twice a run: two reviewer-key tests calleddoctorin a temporary home without hiding the installed client, so the realclaude doctorran there. They now hide it, and a tripwire test fails if a doctor call in a temporary home ever launches it again (#282).
Migration
- A
role_bindingsoverride ofmodelstill wins over the class, so existing overrides keep working. A fork that added a role gives it atier:line. - A Codex install on a provider without these model ids sets
modeltoinheritfor each role underrole_bindings.codex, which restores the previous behavior. - Re-run
harness bmad apply <framework-root>to pick up the revised templates.
[0.10.0] — 2026-09-19
Added
- Add the shared architecture-viewer profile, lifecycle and external process adapter. The preview works with a separately installed protocol 1 viewer and does not bundle or publish that viewer.
- Add bidirectional BMad-to-GitHub issue traceability, deterministic mapping audits and safe fallbacks for repositories that cannot project every native issue type or hierarchy.
- Add a versioned compatibility and migration contract, lifecycle acceptance runner and immutable release-source pinning.
Changed
- Qualify the Claude Code and Codex CLIs on macOS and Linux with version-pinned native evidence. Keep the VS Code surfaces and Codex Desktop as unqualified previews; keep Cursor and Grok planned.
- Preserve released qualification evidence until runtime source changes, then fail the release check rather than silently changing a published support claim.
[0.9.0] — 2026-09-19
Provider-agnostic harness
- Lead with your working style, extensible custom primitives and switchable personal stances.
- Keep one shared authority for rules, stances, skills, roles, workflows and presentation; project it through Claude Code and Codex adapters.
- Add safe native configuration ownership, structural TOML updates, recovery journals, custom homes, drift diagnostics and conflict-preserving uninstall.
- Compose lifecycle policies, strengthen gate invalidation, and normalize usage observations while preserving unknown metrics and detector failures.
- Add custom stance authoring, semantic BMad roles and versioned bidirectional task handoffs.
- Add a native compatibility catalog and release gate. Claude Code and Codex are qualified on the required CLI, VS Code and desktop surfaces; Cursor and Grok, hosted agents, native memory merging and the UML viewer are deferred.
- Coordinate reference-site and personal-site positioning around the same user-aligned primitive model and immutable release identity.
Changed
-
Require a dedicated delivery issue for every PR, with a CI ownership check rejecting missing, multiple, foreign and reused closing issues. Document replacement PRs and the cross-PR race limit.
-
User-authored drafts now defer to an untracked personal voice profile before applying the selected reply-layout stance, and the harness documents where that profile belongs. Accidental typos are explicitly excluded from imitation.
0.8.0 — 2026-09-17
Added
-
docs/getting-started.md: zero to a first useful session for someone who has not used a coding agent before. What the harness is and is not, the prerequisites as a table with a way to check each, install,init, a first session with three things to actually type, what changed about the answers, the five commands with which of them need a code project, what a session costs, and the three commands that diagnose a broken install. The README links it above the install block and from the docs list;SUPPORT.mdnow opens with it and says plainly that everything else there needs a GitHub account and is public. (#82) -
A Before you start block on the README’s first screen: a Claude account on a plan that includes Claude Code — stated, rather than discovered after
installhas already run — the supported platforms,gitand Python 3.9, and which of the rest are optional. (#82) -
A What a session costs section in
docs/preferences.md, naming the rate-limit window, the commands that fan out, and thecoststance as the dial. Nothing previously said that a/researchrun costs several times a plain turn. (#82) -
identity.expertise,expertorbeginner, selecting one paragraph of the personal file. The line telling the agent to communicate at expert level and skip fundamentals was hardcoded inCLAUDE.personal.template.mdand reached every user, including one who had never written code — instructing the agent to withhold exactly the explanation they needed.expertis the default, so a config predating the field resolves to the behaviour it had. (#83) -
Stance presets, asked by
init:softwareis the shipped defaults,generalturns off the commit, test, licensing and build-vs-buy ceremony and lightens plans, for work that is not shipping software. Escaping the professional-SDLC defaults previously meant discovering five separate opt-outs. A preset only supplies the defaults for the questions that follow, so every stance is still asked.delegation,autonomyandcostare untouched by either: how work is spread, how far it runs unattended and what it costs are the same questions whatever the work is. (#83) -
A
voicestance dimension, closing the last always-loaded rule that was a pure preference (#68).scannabledefers to the output style as before and is the default, so nothing changes for an existing install.answer-cardis for reading on a phone: the answer in the first line, then why, the catch, and the alternatives, about 150 words, no tables, with the reasoning left in the file it links rather than re-argued in the message.offimposes no shape at all. The twovoice/detectors are gated on the dimension, so a voice nobody selected is not measured as a violation.
Changed
-
The three rules written entirely about code work say so in their headings —
verification,secretsandconciseness— and one line of the always-loaded preamble states that a rule about repositories, tests or pull requests does not apply elsewhere. Rules link as a directory rather than per file, so they cannot be deselected; the fix is for them to read as inapplicable instead of as instructions about work the reader is not doing. Length-neutral apart from that one line. (#83) -
voice-and-format.mddrops from thirteen lines to six, keeping only what no variant changes: a subagent inherits no voice, so its brief has to carry the output shape itself. Always-loaded context moves 193 to 195 of the 196-line budget. -
A
decisions/no-alternativesdetector, anddecisions-and-plansleavesOPT_OUT. The rule asks for “the alternatives with their honest case”, and nothing measured whether a decision block carried one: the opt-out reasoned from the Review Card and the chooser, and left the clause that does the work unobserved. The detector fires on a final message whose batchedDecisionsblock or recommendation line names no other course, reading the markers out of the raw text so an alternative named inside a quote still counts. A recommendation in running prose is not a decision block and does not fire. (#78) -
harness init: a first-run wizard that writesconfig.jsonby asking for identity one field at a time and offering each stance’s variants with the default in brackets, so configuring the harness no longer means hand-editing JSON. It detects the timezone from/etc/localtimeand the GitHub handle fromghwhen it is logged in, refuses to clobber an existing config without--force, and namesconfig setwhen there is no terminal to ask in. (#81) -
harness config set KEY VALUE, which validates before it writes: an unknown stance variant, stance, identity field, permission posture or top-level key is refused with the options named, rather than being written and failing at the next sync. (#81) -
syncanddoctorreport identity fields still carrying the example file’s value.load_configbackfills fromconfig.example.jsonandrender_personalwrites the result into always-loaded context, so an unedited config had the agent address you as the placeholder name with nothing on screen to say so. Reported, never fatal: a dry-run sync against the example config still succeeds. (#81)
Fixed
-
installandsyncrefuse to run on Windows and name WSL2, rather than half-working: the harness links into~/.claudewith symlinks and every hook is a POSIX command. Nothing in the README or the docs had ever said which platforms are supported._runalso treats an absolute path that does not exist as a missing tool, so/bin/bashbeing absent reports rather than raises. (#82) -
doctornow reports whether each registered hook can actually run, instead of printing an executable bit that never mattered. It resolves the interpreter and the script path of every hook command in the live settings and names what is missing. Claude Code treats a hook that fails to start as non-blocking, so a machine withoutpython3on PATH, or an install that was never synced, turned all eleven hooks into silent no-ops — the command grader that asks before something irreversible and the stop gate that runs the repository’s checks among them. Both guards disappeared with nothing on screen to say so. (#85) -
The two hook messages a user actually sees are written for a reader now. The grade-bash denial said “No prompt exists in this mode” and told them to re-run with a marker; it now says the command was refused because nothing can prompt, and what to say before running it again. Every grade carries its meaning in words — “this cannot be undone” — alongside the label. The stop gate says where the failing command came from, and the untrusted-folder notice names the command that fixes it. (#85)
-
The four commands that assume a git repository now check for one before they start, and name a fallback instead of stopping dead.
/buildsays up front that it needs a repository, a remote andgh, offers to make the change in place when there is no repository, and stops at the local commit when there is no remote — rather than failing atgh pr createwith the work already done./reviewsays there is no diff to review outside a repository, before it spawns either pass, and offers named files or a pasted patch./planand/handoffwrite beside the work in the current directory when there is no repository root, and/plannow says outright that it needs neither a repository nor code. (#84) -
bin/harness installno longer ends in a traceback on a machine that lacksghornpm.subprocess.runraisesFileNotFoundErrorwhen argv[0] does not exist andcheck=Falsesuppresses only a non-zero exit, so the unguardedgh auth statusat the end of every install and thenpm install -g @openai/codexstep both crashed rather than reported — reachable with--no-brewon macOS and on every Linux run. Every external call now goes through one helper that resolves the executable first, names it when it is missing, and carries on. The lookup also searches the keg-onlynode@22bin directory, which Homebrew does not link into its prefix, sonpmis found after a plainbrew installon a machine with no other node. (#80)
0.7.0 — 2026-09-17
Added
-
A
code-quality-instrumentsskill: branch coverage over line coverage, mutation score as the only instrument here that measures assertions rather than execution, complexity joined to coverage to rank the risky functions, and duplication as a refactor signal that is never a gate. Carries per-language instruments for Python, TypeScript, Rust and Go, and the operating rules that keep them usable: mutate the diff rather than the tree, run one instrument at a time, bound the workers. Thetesting: requiredvariant gains a one-line pointer, which is the whole always-loaded cost. Adapted fromunclebob/swarm-forge, whose engineering article pins real instruments per language where our stance only asked that tests exist. (#73) -
An eleventh hook,
brief-guard: PreToolUse onAgent, it appends a 400-word return bound to a subagent brief that states none, rather than asking the orchestrator to write one.transcript-hygiene/brief-without-capfired 536 times across 30 percent of sessions, so the prose was not working. What counts as a bound and which agents are exempt are imported fromrule-detectors.pyrather than copied, and a test asserts the appended text satisfies the detector, since a bound the detector cannot see would never move the number. Adapted fromunclebob/swarm-forge, whose handoff helper fills the commit SHA so the agent never types one. No-op underdelegation: off, wheretier-agent-spawnsalready gates the spawn. (#72) -
A
commit-msghook intemplates/repo/hooks/, installed per repository, refusing a subject that is not a Conventional Commit. It reads thecommitsstance and does nothing underofforas-you-go; underconventional-attributedit also warns on a missingCo-Authored-By:trailer, which it cannot add because the model name is session state a git hook cannot see. Adapted fromunclebob/swarm-forge, which appends its byline the same way. A git hook was the right surface because it sees the final message however it was written, where thecommits/non-conventionaldetector parses only-mand undercounts. (#71)
0.6.1 — 2026-09-17
Fixed
- The instruction to gather with subagents unasked no longer loads under every stance. It sat in
claude/CLAUDE.mdandclaude/rules/delegation.md, both always-loaded, and contradicted thedelegation: offvariant outright, so the selection did not decide the behaviour it named. It now lives in thetieredandsession-modelvariants that mean it, and a regression test asserts that no always-loaded file carries one. Always-loaded context drops from 195 to 192 lines. (#67)
Changed
- The README,
docs/how-it-works.mdanddocs/preferences.mdsay which layer is switchable and which is the floor, name the test a stance has to pass, and list the rules that do not pass it yet instead of leaving them implicit. (#67, #68)
0.6.0 — 2026-09-17
Added
- A rule-detector registry (
claude/hooks/rule-detectors.py): fourteen deterministic detectors over a session transcript, one or more per always-loaded rule, with an explicit opt-out list for the three rules nothing in a transcript can decide.harness lintnow fails on a rule with neither, and reads its secret patterns from the same module. (#58) - A tenth hook,
grade-bash: every Bash command is graded 0–3 on the read-only grammar’s decomposition (read-only, local, remote-mutating, irreversible) and the autonomy stance sets the gate:executeon grade 3,confirm-writeson 2 and up,askon 1 and up. Prompting modes getaskwith a one-line reason;autoandbypassPermissions, where a hook’saskis ignored, getdenywith the same reason and a chat-confirmed re-run prefixedHARNESS_CONFIRMED=1. (#60) - The session-end worker runs the detector registry and records
rules,countsandstancesper session;harness usage --rules [--by rule|repo|stance]reports hits, sessions and share per detector, markspromote?above 30 percent of at least 20 sessions andunobservedat zero over at least 20, and--rescanbackfills from existing transcripts. (#59)
Changed
- The README leads with the positioning line; the docs explain command grades, the ask-versus-deny split per permission mode with the hooks-reference quotes, and rule telemetry as the signal for pruning the always-loaded rules. (#61)
0.5.1 — 2026-09-16
Changed
- Provenance credits a year of coding agents, most of it in Cursor and the last two months in
Claude Code, rather than a year in
~/.claude. - The README and the comparison no longer claim the read-only hook is what stops plan mode prompting: Claude Code’s own classifier does that by default in auto mode. The hook’s value is deterministic, no-round-trip approval that also covers Manual mode and API, Bedrock and Vertex sessions.
0.5.0 — 2026-09-16
Added
- A
tier-agent-spawnshook (PreToolUseonAgent) applies thedelegationstance to every subagent spawn that names no agent definition and no model, which is what a planning framework’s or a plugin’s “launch a subagent” produces: one tier below the session undertiered, untouched undersession-model, a prompt underoff. Frontmatter and explicit model choices are left alone. Inside a repository that carries a framework runtime (_bmad/scripts/or_bmad/core/), a bare spawn keeps the session model: the framework’s lenses are judgment work its overrides cannot rename, and its own rule is same capability. templates/bmad/custom/carries override files in BMad’s own format forbmad-build,bmad-build-autoandbmad-code-reviewthat run their review layers as thereviewerandspec-revieweragents and the implementation handoff on the builder’s tier.harness bmad applyinstalls them into a repository’s_bmad/custom/, skipping a template whose keys or layer ids the installed skill no longer declares, andharness bmad checkreports that drift. Theharness-sessionhook runsapplyfor the repository a session starts in and says so once when it wrote, kept or skipped a file.docs/bmad.mdrecords how the two pieces keep the delegation stance in charge of a framework’s spawns, and why the implementation handoff is not routed tobuilder.
0.4.1 — 2026-09-16
Security
- The
readonly-bashhook approved any command placed after a#comment and a newline: the lines were joined with;before tokenizing, so shlex’s comment handling swallowed the rest and only the benign prefix was judged. Each line now loses its own comment first and a#inside a word stays part of the word, as in bash. Found by an independent review of 0.4.0.
Fixed
- A bare
date MMDDhhmmoperand, which sets the system clock, falls through to the prompt. harness uninstallremoves the PATH line it added to~/.zprofileand the ignore block it added to~/.config/git/ignore, leaving every other line in place.
Changed
- CI runs
harness sync --dry-runagainst the example config, as CONTRIBUTING said it did. - The README points at
--helpfor the full command surface and namesconfig get;docs/preferences.mddocuments thevscode.manageandcodex.managekeys.
0.4.0 — 2026-09-16
Added
harness trust <path>records a repository root whose## Gateblock the stop gate may run, and--removeforgets it; the list lives in~/.config/agent-harness/trusted.txt.
Security
- The
stop-gatehook runs a repository’s## Gateblock only in a folder whose trust dialog has been accepted in Claude Code, or whose root is listed by the newharness trust <path>command, so a freshly cloned repository can no longer run commands on the first Stop. Until then it skips with a note on stderr. - The
sessionhook frames the handoff file it injects at session start as repository content on both sides, to be treated as data rather than instruction, the way theneutralizehook frames tool output.
Fixed
- The
readonly-bashhook approved several commands that write or execute: a second command after a newline,awkscripts callingsystem()or redirecting output,envwith a program argument,fd -x,rg --pre,sed -nscripts usingwore,sort -o,tree -o,yq -i,git grep --open-files-in-pager, aPATH=orGIT_*=prefix, any binary run by absolute path with a read-only name, and the>|,&>>,>&and<>redirections. Each now falls through to the permission prompt.tests/test_allow_readonly_bash.pypins the corpus. - The settings template no longer carries the allow rules that granted the same primitives
without the hook:
Bash(awk *),Bash(sort *),Bash(sed -n *),Bash(fd *),Bash(rg *),Bash(tree *),Bash(file *),Bash(date *)and the leading-wildcardBash(* --version). The hook approves the safe invocations of every one of them.
Changed
harness syncremoves an allow rule that an earlier template added and the current one has dropped, while still keeping rules the user added themselves.
0.3.0 — 2026-09-16
Added
- A
plan-webfetchPreToolUse hook that approvesWebFetchwhile in plan mode, forhttp/httpsURLs only, so gathering context for a plan no longer prompts for every documentation URL. Every other permission mode is unchanged, and theneutralizehook still scans the fetched text (#37).
Changed
- The
readonly-bashhook now decomposes compound commands —;/&&/pipelines,for/while/until/ifblocks, subshell( )and group{ }, and command substitutions$(...), backticks and<(...)— and approves the whole only when every command inside is read-only, so plan mode stops prompting for read-only loops and substitutions. A write anywhere still falls through, and a reserved word used as an argument, such asgrep -q done, is treated as data rather than syntax (#37).
0.2.0 — 2026-09-16
Added
permissions.denyrules in the settings template for credential files and generated directories, so the file tools and the Bash commands that read files are both covered.- Three subagent definitions —
gatherer,reviewerandlog-compressor— carrying their model, effort level and tool list, so the delegation tiers hold without a retyped brief. - A
sandboxskill: container or built-in sandbox posture for an unattended loop. - A
filter-outputPreToolUse hook that pipes a test, build, lint or type-check run through a line filter, keeping failures, summaries and the tail while preserving the exit status. - A
coststance withfrugal,balancedandmaxvariants, and acache-hygienerule. - A
neutralizePostToolUse hook that flags instruction-shaped text inBash,WebFetchandReadoutput, advisory only, never blocking or rewriting a result. - A
stop-gateStop hook that runs the fenced## Gateblock of a repository’sAGENTS.mdand refuses to end the turn while it is red, bounded by a block count and a time budget. - Four slash commands —
/research,/plan,/buildand/review— each composing skills the harness already ships. - A
usage-logSessionEnd hook andharness usage, reporting per-session tokens and cache hit rate from a local file; seedocs/usage.md. - A
/handoffcommand that writes.claude/progress.mdand promotes durable learnings into a dated file underdocs/solutions/, and a SessionStart hook that reads that progress file and the last five commits back as context. The progress file is added to the global git ignore. - A
builderagent carrying the standing implementation brief — a worktree of its own, tests with every change, the repository’s gate, one local Conventional Commit and no push — so/buildspawns it and verifies the gate itself instead of retyping the steps. - A
spec-revieweragent that reports scope deviations only, and a two-stage/reviewthat runs it before the qualityreviewer, each in a context that has not seen the other’s findings. - A
planneragent carrying the Review Card contract, so/plancan hand the file-writing to a fresh context and keep its own for the review conversation. - A
design-judgeagent carrying the design loop’s scored critique and its hard gates, so the independent judge is a fixed definition the skill names rather than a rubric pasted each round.
Fixed
- The agent sync tests restore the environment they change, so a later test in the same run is no longer affected by them.
Changed
- Always-loaded context (
claude/CLAUDE.md, every rule, and the longest variant of each stance) is capped at 200 lines andharness lintfails with a per-group breakdown when it is exceeded. The rules keep their operative lines and point at the skill holding the reasoning; the rationale, examples and evidence moved verbatim intodelegation-tiering,plan-authoring,harness-authoring, the newtranscript-hygieneandapi-verificationskills,docs/how-it-works.mdanddocs/preferences.md. 583 lines before, 185 after.
0.1.1 — 2026-09-16
The repository was re-created with a fresh history for this release. The 0.1.0 tag and its history are gone: the lint in that release embedded a denylist of the maintainer’s own identifiers, which is a disclosure of the very values it existed to catch.
Changed
- The lint carries no list of real values. It matches personal-data shapes (12-digit account
ids, email addresses, home-directory paths, cloud ARNs, hosted-zone ids, identity-provider
tenants, private IPs) and secret patterns everywhere in the tree with no file exempt, derives
the maintainer’s name from
LICENSEandCODEOWNERS, and reads personal terms from the untracked~/.config/agent-harness/lint-terms.txt(seelint-terms.example.txt). harness lint --stagedand a.githooks/pre-commithook, installed byharness sync, lint every commit in the checkout before it is made.permissions: bypassis refused unlesspermissions_bypass_acknowledgedistruein the config file, and is documented as unsuitable for any machine that touches regulated data.- Test fixtures assemble identifier-shaped strings at run time, so the tests are linted like everything else; a regression test asserts no file in the tree carries an identifier shape.
[0.1.0] — 2026-09-16 (withdrawn)
Added
- Nine core rules, seven stances with twenty variants, ten skills, three hooks and the Scannable output style, extracted from a working harness and rewritten in second person.
bin/harnesswithinstall,sync,diff,doctor,uninstall,lint,workspace createandconfig get; standard library only.- Symlink-based live sync into
~/.claude, ownership-scoped settings merge, identity rendered from~/.config/agent-harness/config.json, per-sessionHARNESS_*overrides. - VS Code owned settings and extension lists; generated Codex
AGENTS.md; repo starter templates. - Community files, issue and PR templates, CI with lint and tests, Dependabot for actions.